Skip to content

Commit 287687d

Browse files
authored
feat: add internal code mode namespaces (#88043)
* feat: add internal code mode namespaces * test: add code mode namespace live proof * test: add live code mode Docker repro * chore: keep code mode docker repro out of package scripts * fix: break code mode namespace type cycle * fix: clean code mode namespace ci drift * fix: route code mode namespaces through tools * fix: preserve explicit agent global sessions * docs: explain code mode namespace registry * test: cap realtime websocket payload * fix: normalize code mode timeout results * fix: satisfy code mode timeout lint * chore: rerun code mode CI * ci: extend node shard silence watchdog * test: avoid child process mock deadlocks * test: fix code mode repro shebang * fix: scope explicit agent sentinel sessions * test: preserve child process mock actual loader * fix: dispatch namespace tools by exact id * test: satisfy restart execFile mock type
1 parent 22e4289 commit 287687d

23 files changed

Lines changed: 2266 additions & 54 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,7 @@ jobs:
11511151
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
11521152
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
11531153
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
1154+
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "900000"
11541155
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
11551156
shell: bash
11561157
run: |

docs/reference/code-mode.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ read_when:
66
- You want to enable OpenClaw code mode for an agent run
77
- You need to explain why code mode is different from Codex Code mode
88
- You are reviewing the exec/wait contract, QuickJS-WASI sandbox, TypeScript transform, or hidden tool-catalog bridge
9+
- You are adding or reviewing an internal code-mode namespace registry integration
910
---
1011

1112
Code mode is an experimental OpenClaw agent-runtime feature. It is off by
@@ -380,6 +381,7 @@ The guest runtime exposes a small global API:
380381
```typescript
381382
declare const ALL_TOOLS: ToolCatalogEntry[];
382383
declare const tools: ToolCatalog;
384+
declare const namespaces: Record<string, unknown>;
383385

384386
declare function text(value: unknown): void;
385387
declare function json(value: unknown): void;
@@ -433,6 +435,189 @@ const hits = await tools.web_search({ query: "OpenClaw code mode" });
433435
The guest runtime must not expose host objects directly. Inputs and outputs cross
434436
the bridge as JSON-compatible values with explicit size caps.
435437

438+
## Internal namespaces
439+
440+
Internal namespaces give code mode a concise domain API without adding more
441+
model-visible tools. A loader-owned integration can register a namespace such
442+
as `Issues`, `Fictions`, or `Calendar`; guest code then calls that namespace
443+
inside the QuickJS program while OpenClaw still shows only `exec` and `wait` to
444+
the model.
445+
446+
Namespaces are internal for now. There is no public plugin SDK namespace API:
447+
external plugin namespaces need a loader-owned contract so plugin identity,
448+
installed manifests, auth state, and cached catalog descriptors cannot drift
449+
from the plugin tools that back the namespace. Core code mode owns only the
450+
sandbox, serialization, catalog gating, and bridge dispatch.
451+
452+
Guest code can then use either the direct global or the `namespaces` map:
453+
454+
```javascript
455+
const open = await Issues.list({ state: "open" });
456+
const alsoOpen = await namespaces.Issues.list({ state: "open" });
457+
return { count: open.length, alsoCount: alsoOpen.length };
458+
```
459+
460+
### Registry lifecycle
461+
462+
The namespace registry is process-local and keyed by namespace id. A typical
463+
run follows this path:
464+
465+
1. A trusted loader calls `registerCodeModeNamespaceForPlugin(pluginId, registration)`.
466+
2. Code mode creates the hidden `ToolSearchRuntime` for the run and reads its
467+
run-scoped catalog.
468+
3. `createCodeModeNamespaceRuntime(ctx, catalog)` keeps only registrations
469+
whose `requiredToolNames` are all visible and owned by the same `pluginId`.
470+
4. Each visible namespace calls `createScope(ctx)` for the current run. The
471+
scope receives run context such as `agentId`, `sessionKey`, `sessionId`,
472+
`runId`, config, and abort state.
473+
5. Scope data is serialized into a plain descriptor and injected into QuickJS as
474+
direct globals and `namespaces.<globalName>`.
475+
6. Guest calls suspend through the worker bridge, resolve the namespace path on
476+
the host, map the call to a declared plugin-owned catalog tool, and execute
477+
that tool through `ToolSearchRuntime.call`.
478+
7. `wait` resumes the same namespace runtime when a code-mode run suspended on
479+
nested tool work.
480+
8. Plugin rollback or uninstall calls `clearCodeModeNamespacesForPlugin(pluginId)`
481+
so stale globals do not survive a failed plugin load.
482+
483+
The important invariant: namespace calls are catalog tool calls. They use the
484+
same policy hooks, approvals, abort handling, telemetry, transcript projection,
485+
and suspend/resume behavior as `tools.call(...)`.
486+
487+
### Registration shape
488+
489+
Register namespaces from the integration that owns the backing tools. Keep the
490+
scope small and only expose domain verbs that map to declared catalog tools.
491+
492+
```typescript
493+
import {
494+
createCodeModeNamespaceTool,
495+
registerCodeModeNamespaceForPlugin,
496+
} from "../agents/code-mode-namespaces.js";
497+
498+
const pluginId = "github";
499+
500+
registerCodeModeNamespaceForPlugin(pluginId, {
501+
id: "github-issues",
502+
globalName: "Issues",
503+
description: "GitHub issue helpers for the current repository.",
504+
requiredToolNames: ["github_list_issues", "github_update_issue"],
505+
prompt: "Use Issues.list(params) and Issues.update(number, patch).",
506+
createScope: (ctx) => ({
507+
repository: ctx.config,
508+
list: createCodeModeNamespaceTool("github_list_issues", ([params]) => params ?? {}),
509+
update: createCodeModeNamespaceTool("github_update_issue", ([number, patch]) => ({
510+
number,
511+
patch,
512+
})),
513+
}),
514+
});
515+
```
516+
517+
`createCodeModeNamespaceTool(toolName, inputMapper)` marks a scope member as a
518+
callable namespace function. The optional `inputMapper` receives the guest
519+
arguments and returns the input object for the backing catalog tool. Without an
520+
input mapper, the first guest argument is used, or `{}` when omitted.
521+
522+
Raw host functions are rejected before guest code runs:
523+
524+
```typescript
525+
createScope: () => ({
526+
// Wrong: this bypasses the catalog tool lifecycle and will be rejected.
527+
list: async () => githubClient.listIssues(),
528+
});
529+
```
530+
531+
### Ownership and visibility
532+
533+
Namespace ownership is bound to the registration caller's `pluginId`.
534+
`requiredToolNames` is both a visibility gate and an ownership check:
535+
536+
- every required tool must exist in the run catalog
537+
- every required tool must have `sourceName === pluginId`
538+
- the namespace is hidden when any required tool is absent or owned by another
539+
plugin
540+
- each callable path may target only a tool named in `requiredToolNames`
541+
542+
This prevents another plugin from exposing a namespace by registering a
543+
same-named tool. It also keeps namespaces aligned with ordinary agent policy:
544+
if the run cannot see the backing tools, it cannot see the namespace.
545+
546+
For example, a GitHub namespace should live behind a GitHub-owned extension that
547+
owns GitHub auth, REST or GraphQL clients, rate limits, write approvals, and
548+
tests. Core code mode should not embed GitHub-specific APIs, token handling, or
549+
provider policy.
550+
551+
### Scope serialization rules
552+
553+
`createScope(ctx)` may return a plain object containing JSON-compatible values,
554+
arrays, nested objects, and `createCodeModeNamespaceTool(...)` call markers.
555+
Host objects never enter QuickJS directly.
556+
557+
The serializer rejects:
558+
559+
- raw functions
560+
- circular object graphs
561+
- unsafe path segments: `__proto__`, `constructor`, `prototype`, empty keys, or
562+
keys containing the internal path separator
563+
- `globalName` values that are not JavaScript identifiers
564+
- `globalName` collisions with built-in code-mode globals such as `tools`,
565+
`namespaces`, `text`, `json`, `yield_control`, or `__openclaw*`
566+
567+
Values that cannot be JSON-serialized are converted to JSON-safe fallback
568+
values before crossing the bridge. Binary data, handles, sockets, clients, and
569+
class instances should stay behind ordinary catalog tools.
570+
571+
### Prompts
572+
573+
The namespace `description` and optional `prompt` are appended to the model
574+
visible `exec` schema only when the namespace is visible for that run. Use them
575+
to teach the smallest useful surface:
576+
577+
```typescript
578+
{
579+
description: "Fiction production service helpers.",
580+
prompt:
581+
"Use Fictions.riskAudit(), Fictions.promoteIfReady(id, status), and Fictions.unpaidOver(amount).",
582+
}
583+
```
584+
585+
Keep prompts about the namespace contract, not auth setup, implementation
586+
history, or unrelated plugin behavior.
587+
588+
### Cleanup
589+
590+
Namespaces are process-local registrations. Remove them when the owning plugin
591+
is disabled, uninstalled, or rolled back:
592+
593+
```typescript
594+
clearCodeModeNamespacesForPlugin(pluginId);
595+
```
596+
597+
Use `unregisterCodeModeNamespace(namespaceId)` only when removing one known
598+
namespace. Tests can call `clearCodeModeNamespacesForTest()` to avoid leaking
599+
registrations across cases.
600+
601+
### Test checklist
602+
603+
Namespace changes should cover the security boundary and the guest behavior:
604+
605+
- namespace prompt text appears only when backing tools are visible
606+
- same-named tools from another `sourceName` do not expose the namespace
607+
- raw scope functions are rejected
608+
- forged namespace ids and forged paths are rejected
609+
- callable paths cannot target undeclared tools
610+
- nested objects and shared references serialize correctly
611+
- namespace calls execute through catalog tools and return JSON-safe details
612+
- failures can be caught by guest code
613+
- suspended namespace calls resume through `wait`
614+
- plugin rollback clears the owning namespace registrations
615+
616+
Namespaces complement the generic `tools.search` / `tools.call` catalog. Use
617+
the catalog for arbitrary enabled tools; use namespaces for plugin-owned,
618+
documented domain APIs where concise code is more reliable than repeated schema
619+
lookups.
620+
436621
## Output API
437622

438623
`text(value)` appends human-readable output to the `output` array.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5+
ROOT_DIR="${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$SCRIPT_ROOT_DIR}"
6+
ROOT_DIR="$(cd "$ROOT_DIR" && pwd)"
7+
TRUSTED_HARNESS_DIR="${OPENCLAW_LIVE_DOCKER_TRUSTED_HARNESS_DIR:-$SCRIPT_ROOT_DIR}"
8+
TRUSTED_HARNESS_DIR="$(cd "$TRUSTED_HARNESS_DIR" && pwd)"
9+
source "$TRUSTED_HARNESS_DIR/scripts/lib/docker-e2e-image.sh"
10+
11+
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-code-mode-namespace-live-e2e" OPENCLAW_CODE_MODE_NAMESPACE_LIVE_E2E_IMAGE)"
12+
SKIP_BUILD="${OPENCLAW_CODE_MODE_NAMESPACE_LIVE_E2E_SKIP_BUILD:-0}"
13+
PROFILE_FILE="${OPENCLAW_CODE_MODE_NAMESPACE_LIVE_PROFILE_FILE:-${OPENCLAW_TESTBOX_PROFILE_FILE:-$HOME/.openclaw-testbox-live.profile}}"
14+
run_log=""
15+
if [ ! -f "$PROFILE_FILE" ] && [ -f "$HOME/.profile" ]; then
16+
PROFILE_FILE="$HOME/.profile"
17+
fi
18+
19+
cleanup() {
20+
if [ -n "${run_log:-}" ]; then
21+
rm -f "$run_log"
22+
fi
23+
}
24+
trap cleanup EXIT
25+
26+
docker_e2e_build_or_reuse "$IMAGE_NAME" code-mode-namespace-live "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
27+
28+
PROFILE_MOUNT=()
29+
PROFILE_STATUS="none"
30+
if [ -f "$PROFILE_FILE" ] && [ -r "$PROFILE_FILE" ]; then
31+
set -a
32+
# shellcheck disable=SC1090
33+
source "$PROFILE_FILE"
34+
set +a
35+
PROFILE_MOUNT=(-v "$PROFILE_FILE":/home/appuser/.profile:ro)
36+
PROFILE_STATUS="$PROFILE_FILE"
37+
fi
38+
39+
echo "Running code mode namespace live Docker E2E..."
40+
echo "Profile file: $PROFILE_STATUS"
41+
run_log="$(docker_e2e_run_log code-mode-namespace-live)"
42+
if ! docker_e2e_run_with_harness \
43+
--user root \
44+
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
45+
-e OPENAI_API_KEY \
46+
-e OPENAI_BASE_URL \
47+
-e "OPENCLAW_CODE_MODE_LIVE_MODEL=${OPENCLAW_CODE_MODE_LIVE_MODEL:-gpt-5.4-mini}" \
48+
-e "OPENCLAW_CODE_MODE_LIVE_TASKS=${OPENCLAW_CODE_MODE_LIVE_TASKS:-3}" \
49+
-v "$ROOT_DIR":/src:ro \
50+
"${PROFILE_MOUNT[@]}" \
51+
"$IMAGE_NAME" \
52+
bash /src/scripts/repro/code-mode-namespace-live-scenario.sh >"$run_log" 2>&1; then
53+
docker_e2e_print_log "$run_log"
54+
exit 1
55+
fi
56+
57+
docker_e2e_print_log "$run_log"
58+
echo "Code mode namespace live Docker E2E passed"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
source scripts/lib/live-docker-stage.sh
5+
6+
for profile_path in "$HOME/.profile" /home/appuser/.profile; do
7+
if [ -f "$profile_path" ] && [ -r "$profile_path" ]; then
8+
set +e +u
9+
# shellcheck disable=SC1090
10+
source "$profile_path"
11+
set -euo pipefail
12+
break
13+
fi
14+
done
15+
16+
if [ -z "${OPENAI_API_KEY:-}" ]; then
17+
echo "ERROR: OPENAI_API_KEY is required for the code mode namespace live Docker test." >&2
18+
exit 1
19+
fi
20+
export OPENAI_API_KEY
21+
if [ -n "${OPENAI_BASE_URL:-}" ]; then
22+
export OPENAI_BASE_URL
23+
fi
24+
25+
tmp_dir="$(mktemp -d)"
26+
cleanup() {
27+
rm -rf "$tmp_dir"
28+
}
29+
trap cleanup EXIT
30+
31+
openclaw_live_stage_source_tree "$tmp_dir"
32+
openclaw_live_stage_node_modules "$tmp_dir"
33+
openclaw_live_link_runtime_tree "$tmp_dir"
34+
35+
cd "$tmp_dir"
36+
tsx scripts/repro/code-mode-namespace-live.ts

0 commit comments

Comments
 (0)