@@ -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
1112Code 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
381382declare const ALL_TOOLS: ToolCatalogEntry [];
382383declare const tools: ToolCatalog ;
384+ declare const namespaces: Record <string , unknown >;
383385
384386declare function text(value : unknown ): void ;
385387declare function json(value : unknown ): void ;
@@ -433,6 +435,189 @@ const hits = await tools.web_search({ query: "OpenClaw code mode" });
433435The guest runtime must not expose host objects directly. Inputs and outputs cross
434436the 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.
0 commit comments