mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Add browser session instruction injection
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# Browser System Instructions Injection
|
||||
|
||||
## Problem
|
||||
|
||||
The browser harness can run a real Codex turn, but the `wasm32` build does not
|
||||
load project docs from `AGENTS.md` today. In the browser, the application often
|
||||
knows important environment-specific policy that the harness does not know:
|
||||
|
||||
- code runs inside AppKernel;
|
||||
- browser APIs such as OPFS and network access are available;
|
||||
- only a specific OPFS path layout is allowed;
|
||||
- only specific GitHub URL prefixes are allowed; and
|
||||
- the agent is expected to fetch, cache, and search source code itself.
|
||||
|
||||
Those details need to reach the model as prompt-layer instructions even when the
|
||||
harness cannot discover them from the filesystem.
|
||||
|
||||
## Goals
|
||||
|
||||
- Let browser applications inject session-scoped prompt instructions.
|
||||
- Keep prompt roles distinct:
|
||||
- base instructions remain the base/system layer;
|
||||
- developer instructions carry application or runtime policy;
|
||||
- user instructions carry AGENTS/project-doc style content.
|
||||
- Keep responsibility boundaries clean:
|
||||
- the application decides where instructions come from;
|
||||
- the harness only accepts and applies resolved instruction text.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Teach the wasm harness to discover `AGENTS.md` on its own.
|
||||
- Move application-specific policy such as AppKernel, OPFS layout, or GitHub
|
||||
allowlists into `codex-core`.
|
||||
- Replace Codex's built-in base prompt.
|
||||
|
||||
## Existing State
|
||||
|
||||
`codex-core` already has the right internal prompt channels:
|
||||
|
||||
- `Config.base_instructions`
|
||||
- `Config.developer_instructions`
|
||||
- `Config.user_instructions`
|
||||
|
||||
The browser adapter does not expose them. `BrowserCodex` currently accepts an
|
||||
API key and a code executor, then builds a session config internally with a
|
||||
fixed cwd. The wasm project-doc loader is also stubbed out, so no `AGENTS.md`
|
||||
content is discovered automatically.
|
||||
|
||||
## Proposed API
|
||||
|
||||
Add a browser-session configuration object that the application can set before
|
||||
starting a turn:
|
||||
|
||||
```ts
|
||||
type BrowserSessionOptions = {
|
||||
cwd?: string;
|
||||
instructions?: {
|
||||
base?: string;
|
||||
developer?: string;
|
||||
user?: string;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Expose it through the browser harness as a session-scoped setter:
|
||||
|
||||
```ts
|
||||
const codex = new BrowserCodex(apiKey);
|
||||
codex.setSessionOptions({
|
||||
cwd: "/workspace",
|
||||
instructions: {
|
||||
developer: "...AppKernel/OPFS/network/GitHub policy...",
|
||||
user: "...resolved AGENTS.md text...",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Prompt Layer Mapping
|
||||
|
||||
### `instructions.developer`
|
||||
|
||||
Use for environment and policy details supplied by the browser application, for
|
||||
example:
|
||||
|
||||
- code runs inside AppKernel;
|
||||
- OPFS and network APIs are available;
|
||||
- approved OPFS path layout;
|
||||
- approved GitHub URL prefixes; and
|
||||
- fetch/cache/search expectations.
|
||||
|
||||
This maps to `Config.developer_instructions`.
|
||||
|
||||
### `instructions.user`
|
||||
|
||||
Use for application-resolved project docs such as `AGENTS.md`.
|
||||
|
||||
This maps to `Config.user_instructions`.
|
||||
|
||||
### `instructions.base`
|
||||
|
||||
Use only as an escape hatch when the embedding application intentionally wants
|
||||
to replace the base instruction bundle for the session.
|
||||
|
||||
This maps to `Config.base_instructions`.
|
||||
|
||||
Most callers should leave this unset so Codex keeps its built-in base prompt.
|
||||
|
||||
## Session Lifetime
|
||||
|
||||
These values are session-scoped, not turn-scoped.
|
||||
|
||||
If `cwd` or any injected instruction changes, the harness should discard the
|
||||
current browser session and create a new one on the next turn. This avoids
|
||||
mixing old prompt state with new environment policy.
|
||||
|
||||
## API Boundary
|
||||
|
||||
### Application responsibilities
|
||||
|
||||
- discover `AGENTS.md` or any equivalent project-doc source;
|
||||
- decide the approved OPFS layout;
|
||||
- decide the approved GitHub URL prefixes;
|
||||
- decide whether AppKernel/network guidance should be present; and
|
||||
- assemble those values into prompt text.
|
||||
|
||||
### Harness responsibilities
|
||||
|
||||
- accept already-resolved session options;
|
||||
- validate and store them;
|
||||
- apply them to the Codex session config;
|
||||
- ensure the configured cwd is used consistently for the session and for turn
|
||||
submission; and
|
||||
- recreate the session when session options change.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Keep the API additive. Existing callers that only set the API key and code
|
||||
executor should continue to work.
|
||||
- Reject unknown fields in the session-options payload so applications do not
|
||||
silently think a field is applied when it is ignored.
|
||||
- Apply the configured cwd both when building the browser config and when
|
||||
calling `Op::UserTurn`.
|
||||
|
||||
## Initial Scope
|
||||
|
||||
Implement the new `BrowserCodex` session-options setter in `wasm-harness`,
|
||||
apply it to config creation, and update the browser demo to act like an
|
||||
embedding application by supplying explicit developer instructions for the
|
||||
browser/AppKernel environment.
|
||||
@@ -29,10 +29,32 @@ webapps can:
|
||||
- depend on `codex-wasm-harness` from a Git branch or local path;
|
||||
- construct `BrowserCodex` from JavaScript or wrap the crate with their own
|
||||
browser bindings;
|
||||
- provide session-scoped prompt inputs such as `cwd`, developer instructions,
|
||||
and user/project-doc instructions;
|
||||
- supply their own browser/runtime implementations for host services such as
|
||||
code execution, event rendering, or persistence; and
|
||||
- keep app-specific browser glue out of the Codex repo.
|
||||
|
||||
The intended boundary is:
|
||||
|
||||
- the application resolves instruction sources such as `AGENTS.md`, AppKernel
|
||||
policy, approved OPFS layout, and approved GitHub URL prefixes;
|
||||
- the harness accepts those resolved strings and injects them into the Codex
|
||||
session as base, developer, or user instructions.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const codex = new BrowserCodex(apiKey);
|
||||
codex.setSessionOptions({
|
||||
cwd: "/workspace",
|
||||
instructions: {
|
||||
developer: "Code runs inside AppKernel. OPFS and network APIs are available.",
|
||||
user: "# AGENTS.md instructions for /workspace\n\n<INSTRUCTIONS>\n...\n</INSTRUCTIONS>",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Current Limitations
|
||||
|
||||
This is a minimal browser port of the Codex turn loop, not full desktop Codex.
|
||||
|
||||
@@ -215,9 +215,27 @@
|
||||
const API_KEY_STORAGE_KEY = "codexWasmHarness.apiKey";
|
||||
const codex = new BrowserCodex(readStoredApiKey());
|
||||
const sandbox = createSandboxKernel();
|
||||
const browserSessionOptions = {
|
||||
cwd: "/workspace",
|
||||
instructions: {
|
||||
developer: [
|
||||
"Code mode runs inside AppKernel, exposed in this demo through a browser iframe sandbox.",
|
||||
"Browser network APIs and OPFS are available to code running in AppKernel.",
|
||||
"Approved OPFS path layout:",
|
||||
"- /workspace: active workspace files and entrypoints",
|
||||
"- /workspace/cache: fetched artifacts and caches",
|
||||
"- /workspace/repos: cloned or mirrored source trees",
|
||||
"Approved GitHub URL prefixes:",
|
||||
"- https://github.com/openai/",
|
||||
"- https://raw.githubusercontent.com/openai/",
|
||||
"Fetch, cache, and search source code yourself using the available browser APIs instead of assuming the harness preloads repository contents.",
|
||||
].join("\n"),
|
||||
},
|
||||
};
|
||||
|
||||
apiKeyInput.value = readStoredApiKey();
|
||||
updateApiKeyStatus();
|
||||
codex.setSessionOptions(browserSessionOptions);
|
||||
codex.set_code_executor(async (requestJson) =>
|
||||
sandbox.execute(requestJson),
|
||||
);
|
||||
|
||||
@@ -32,6 +32,22 @@ const BROWSER_CODEX_HOME: &str = "/codex-wasm";
|
||||
const BROWSER_CWD: &str = "/workspace";
|
||||
const DEFAULT_MODEL: &str = "gpt-5-codex";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct BrowserInstructionOverrides {
|
||||
base: Option<String>,
|
||||
developer: Option<String>,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct BrowserSessionOptions {
|
||||
cwd: Option<String>,
|
||||
#[serde(default)]
|
||||
instructions: BrowserInstructionOverrides,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BrowserRuntimeState {
|
||||
executor: Mutex<Option<JsFunctionHandle>>,
|
||||
@@ -213,6 +229,7 @@ pub struct BrowserCodex {
|
||||
api_key: String,
|
||||
runtime_state: Arc<BrowserRuntimeState>,
|
||||
session: Option<BrowserSession>,
|
||||
session_options: BrowserSessionOptions,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -224,6 +241,7 @@ impl BrowserCodex {
|
||||
api_key,
|
||||
runtime_state: Arc::new(BrowserRuntimeState::default()),
|
||||
session: None,
|
||||
session_options: BrowserSessionOptions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +250,22 @@ impl BrowserCodex {
|
||||
self.session = None;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setSessionOptions)]
|
||||
pub fn set_session_options(&mut self, options: JsValue) -> Result<(), JsValue> {
|
||||
let parsed = parse_browser_session_options(options).map_err(harness_error_to_js)?;
|
||||
if self.session_options != parsed {
|
||||
self.session = None;
|
||||
}
|
||||
self.session_options = parsed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = clearSessionOptions)]
|
||||
pub fn clear_session_options(&mut self) {
|
||||
self.session_options = BrowserSessionOptions::default();
|
||||
self.session = None;
|
||||
}
|
||||
|
||||
pub fn set_code_executor(&mut self, executor: Function) {
|
||||
*self
|
||||
.runtime_state
|
||||
@@ -312,7 +346,7 @@ impl BrowserCodex {
|
||||
text: prompt,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: PathBuf::from(BROWSER_CWD),
|
||||
cwd: session.config.cwd.as_path().to_path_buf(),
|
||||
approval_policy: session.config.permissions.approval_policy.get().clone(),
|
||||
approvals_reviewer: Some(session.config.approvals_reviewer),
|
||||
sandbox_policy: session.config.permissions.sandbox_policy.get().clone(),
|
||||
@@ -354,9 +388,7 @@ impl BrowserCodex {
|
||||
}
|
||||
|
||||
async fn create_session(&self) -> Result<BrowserSession, HarnessError> {
|
||||
let config = build_browser_config()
|
||||
.await
|
||||
.map_err(|error| HarnessError::new(format!("build_browser_config: {error}")))?;
|
||||
let config = build_browser_config(&self.session_options)?;
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key(self.api_key.trim()));
|
||||
let environment_manager = Arc::new(EnvironmentManager::new(None));
|
||||
@@ -384,12 +416,11 @@ impl BrowserCodex {
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_browser_config() -> Result<Config, HarnessError> {
|
||||
let mut config = Config::load_embedded_defaults(
|
||||
PathBuf::from(BROWSER_CODEX_HOME),
|
||||
PathBuf::from(BROWSER_CWD),
|
||||
)
|
||||
.map_err(|error| HarnessError::new(error.to_string()))?;
|
||||
fn build_browser_config(session_options: &BrowserSessionOptions) -> Result<Config, HarnessError> {
|
||||
let cwd = session_options.cwd.as_deref().unwrap_or(BROWSER_CWD);
|
||||
let mut config =
|
||||
Config::load_embedded_defaults(PathBuf::from(BROWSER_CODEX_HOME), PathBuf::from(cwd))
|
||||
.map_err(|error| HarnessError::new(error.to_string()))?;
|
||||
let _ = config.features.enable(Feature::CodeMode);
|
||||
let _ = config.features.enable(Feature::CodeModeOnly);
|
||||
config.model = Some(
|
||||
@@ -398,9 +429,25 @@ async fn build_browser_config() -> Result<Config, HarnessError> {
|
||||
.clone()
|
||||
.unwrap_or_else(|| DEFAULT_MODEL.to_string()),
|
||||
);
|
||||
config.base_instructions = session_options.instructions.base.clone();
|
||||
config.developer_instructions = session_options.instructions.developer.clone();
|
||||
config.user_instructions = session_options.instructions.user.clone();
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn parse_browser_session_options(options: JsValue) -> Result<BrowserSessionOptions, HarnessError> {
|
||||
if options.is_null() || options.is_undefined() {
|
||||
return Ok(BrowserSessionOptions::default());
|
||||
}
|
||||
|
||||
let json = js_sys::JSON::stringify(&options).map_err(js_exception)?;
|
||||
let text = json
|
||||
.as_string()
|
||||
.ok_or_else(|| HarnessError::new("browser session options must be JSON-serializable"))?;
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|error| HarnessError::new(format!("invalid browser session options: {error}")))
|
||||
}
|
||||
|
||||
async fn await_possible_promise(value: JsValue) -> Result<JsValue, HarnessError> {
|
||||
if let Ok(promise) = value.clone().dyn_into::<Promise>() {
|
||||
JsFuture::from(promise).await.map_err(js_exception)
|
||||
@@ -442,3 +489,37 @@ fn js_value_to_string_lossy(value: &JsValue) -> String {
|
||||
fn harness_error_to_js(error: HarnessError) -> JsValue {
|
||||
JsValue::from_str(error.message())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::BrowserInstructionOverrides;
|
||||
use super::BrowserSessionOptions;
|
||||
use super::build_browser_config;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn build_browser_config_applies_session_options() {
|
||||
let config = build_browser_config(&BrowserSessionOptions {
|
||||
cwd: Some("/workspace/repo".to_string()),
|
||||
instructions: BrowserInstructionOverrides {
|
||||
base: Some("base".to_string()),
|
||||
developer: Some("developer".to_string()),
|
||||
user: Some("user".to_string()),
|
||||
},
|
||||
})
|
||||
.expect("browser config");
|
||||
|
||||
assert_eq!(config.cwd.as_path().to_string_lossy(), "/workspace/repo");
|
||||
assert_eq!(config.base_instructions.as_deref(), Some("base"));
|
||||
assert_eq!(config.developer_instructions.as_deref(), Some("developer"));
|
||||
assert_eq!(config.user_instructions.as_deref(), Some("user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_browser_config_defaults_to_workspace_cwd() {
|
||||
let config =
|
||||
build_browser_config(&BrowserSessionOptions::default()).expect("browser config");
|
||||
|
||||
assert_eq!(config.cwd.as_path().to_string_lossy(), "/workspace");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user