@sigrun/ext-codemode 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] - 2026-10-27
9
+ ### Changed
10
+ - Re-released through CI trusted publishing; this version carries SLSA
11
+ provenance. No functional change from 0.1.0.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.1.0] - 2026-10-13
16
+ ### Added
17
+ - Renamed to `@sigrun/ext-codemode` for the 0.1.0 release (repo stays
18
+ `sigrun-ai/sigrun-ext-codemode`): published package metadata
19
+ (`publishConfig` access/provenance), host modules moved to
20
+ `peerDependencies` mirrored in `devDependencies` at exact `1.3.0`
21
+ (`quickjs-wasi` remains the one runtime dependency), and a manifest
22
+ hygiene check wired into `npm test`.
23
+ - sigrun loader-contract wrapper over pi-durable's `createCodemodeExtension`:
24
+ a `CodemodeSandbox` pinned `timeoutMs: 300_000` / `memoryLimitBytes: 256 MiB`
25
+ at construction, MCP-config-driven namespaces (`SIGRUN_MCP_CONFIG` →
26
+ `~/.config/sigrun/mcp.json; snapshot semantics), and the installed-not-offered
27
+ notice section.
28
+ - Boot canary (`scripts/canary.mjs`): wasm resolution + a real harness
29
+ `return 1+1`, fail-loud exit 1 — the image entry pre-step.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael MacLeod
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # @sigrun/ext-codemode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The codemode extension for sigrun (pi-durable): the `codemode` tool —
4
+ model-written JavaScript run in a QuickJS sandbox, whose script survives
5
+ a restart — installed via upstream's
6
+ `createCodemodeExtension` (`@earendil-works/pi-durable/codemode`) with
7
+ sigrun's config and prompt additions.
8
+
9
+ The sigrun extension-package contract: the default export is a factory
10
+ taking no arguments, returning `{ extension }`.
11
+
12
+ ## What the factory does
13
+
14
+ 1. Reads the MCP namespace snapshot ONCE from the same config
15
+ sigrun-ext-mcp reads: `SIGRUN_MCP_CONFIG` env override →
16
+ `~/.config/sigrun/mcp.json`. One `CodemodeNamespace` per enabled
17
+ server, `prefix: "<server>/"`. Absent file → empty namespaces (the
18
+ codemode tool + section still ship). A bad file warns at factory time
19
+ (best-effort) and installs un-namespaced rather than throwing — a bad
20
+ config must not cost the model its codemode tool.
21
+ 2. Builds the QuickJS sandbox (`CodemodeSandbox` from
22
+ `@earendil-works/pi-codemode`) with pinned constructor options:
23
+ `timeoutMs: 300_000`, `memoryLimitBytes: 256 MiB`.
24
+ 3. Wraps upstream's `createCodemodeExtension({ sandbox, namespaces })`
25
+ with a companion `codemode_notice` section (below).
26
+
27
+ Config fields read per server entry (the sigrun-ext-mcp shapes,
28
+ duplicated here because that package is not npm-published): `enabled` /
29
+ `disabled` (either false means off), `exposure`
30
+ (`"codemode"` default | `"deferred"` | `"direct"` | `"hidden"`, plus the
31
+ pi alias `"codemode-deferred"`), and `toolExposure` overrides. A server
32
+ resolving fully `hidden` (server default `hidden` and every override
33
+ `hidden`) is excluded from the snapshot — hidden tools never register,
34
+ so nothing scripts can call remains. Per-tool `hidden` resolution is
35
+ sigrun-ext-mcp's job at registration time; this snapshot only groups by
36
+ prefix, and hidden tools never reach `agent.callable` to be grouped.
37
+
38
+ ### The deadline (verified against pi-codemode 1.2.0)
39
+
40
+ The sandbox constructor's `timeoutMs` governs the plain
41
+ `sandbox.execute()` surface. The durable codemode tool path
42
+ (`sandbox.start()`) takes its deadline from the script's own first line:
43
+
44
+ ```js
45
+ // @options: {"timeout_ms": 60000}
46
+ while (true) {}
47
+ ```
48
+
49
+ A script without an options line has NO deadline (the constructor cap
50
+ does not apply there) — models should be prompted to set one; sigrun's
51
+ system prompt carries that guidance. The pinned constructor options
52
+ still bound the plain sandbox surface and document the ceiling.
53
+
54
+ ## The warn-if-not-offered section
55
+
56
+ Upstream's `codemode` section lists script-callable tools (grouped by
57
+ namespace) but does not know whether the `codemode` tool itself is
58
+ model-offered. When the extension is installed yet the tool is excluded
59
+ from a request's offered set (Settings → default tools), the companion
60
+ `codemode_notice` section renders:
61
+
62
+ > codemode scripts are available but the tool is not offered; enable it
63
+ > via Settings → default tools
64
+
65
+ When the tool is offered the section renders NOTHING (returns
66
+ `undefined`; no tag is written) — de-duplication with upstream's listing
67
+ by construction. Mechanism choice: a companion section rather than a
68
+ `wrapSection` on upstream's key — the wrap could only prepend text
69
+ around upstream's render, while the companion keys the notice to the
70
+ not-offered condition exactly and leaves the upstream render untouched.
71
+ Both sections are registered by the one extension this package installs,
72
+ so the pair always travels together.
73
+
74
+ ## Tool naming: `<server>/<tool>` (decision, agreeing with sigrun-ext-mcp)
75
+
76
+ Tools keep the `<server>/<tool>` shape rather than upstream pi's
77
+ `mcp__<server>__<tool>`; the codemode namespaces use
78
+ `prefix: "<server>/"`, so upstream's longest-prefix grouping renders per
79
+ server with no renames. The same decision is recorded in
80
+ sigrun-ext-mcp's README; the two must stay consistent.
81
+
82
+ ## Package replacement semantics (slice 4)
83
+
84
+ Replace this package by not installing it and installing yours: the
85
+ surface is one tool named exactly `codemode` (plus the companion
86
+ `codemode_notice` section and the factory-time namespace snapshot), so a
87
+ per-name replacement is trivial — uninstall ours, install the replacement
88
+ under the same tool name. Do NOT co-install: pi-durable resolves
89
+ cross-extension duplicate tool names last-selected-wins, so a co-installed
90
+ replacement silently shadows the original. Sigrun's extension loader now
91
+ makes the shadowing visible at boot (it `console.warn`s once per duplicate
92
+ tool name, naming the extensions and the last-wins resolution), but the
93
+ warning changes nothing: last-wins stands. Replace, don't co-install.
94
+
95
+ ## Dependencies
96
+
97
+ `quickjs-wasi` is a REAL dependency of this package, not a transitive
98
+ courtesy: pi-codemode's worker resolves `quickjs-wasi/quickjs.wasm`
99
+ from disk at the first script. A re-pinned or deduped-away transitive
100
+ copy strands the wasm file, and the first codemode call fails at runtime
101
+ (in prod: a 500 on the first script). The canary test
102
+ (`test/canary.spec.ts`) asserts the wasm resolves and runs
103
+ `return 1 + 1` against the BUILT package — the same assertions Task 8
104
+ wires into the container image build.
105
+
106
+ ## Development
107
+
108
+ ```sh
109
+ npm install && npm test # vitest run (includes the canary + the runaway test, last)
110
+ npm run typecheck
111
+ npm run build
112
+ ```
113
+
114
+ The runaway-script test (`while (true) {}` with a 1s `timeout_ms`) is
115
+ the LAST test in `test/codemode.test.ts`, so a wall-clock death on it
116
+ still ships the rest of the suite.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The codemode config snapshot: the mcp.json-derived namespace list.
3
+ *
4
+ * Reuses the MCP config RESOLUTION and the namespace-relevant parse SHAPES
5
+ * from sigrun-ext-mcp (rev-3 ruling accepts the duplicated parse of the
6
+ * config file across repos; sigrun-ext-mcp is not npm-published, so its
7
+ * `parseMcpConfig` cannot be imported here — the shapes are duplicated
8
+ * with the derivations quoted below for review parity).
9
+ *
10
+ * Resolution order (identical to sigrun-ext-mcp src/config.ts):
11
+ * 1. `SIGRUN_MCP_CONFIG` env override — an explicit path to an mcp.json
12
+ * 2. `~/.config/sigrun/mcp.json` (the sigrun config dir)
13
+ *
14
+ * What we read from each server entry:
15
+ * - `serverEnabled` (mcp repo's derivation): `enabled: false` OR
16
+ * `disabled: true` skips the server entirely.
17
+ * - Exposure (mcp repo's shape): a server-default `exposure` word, plus
18
+ * `toolExposure` overrides mapping exact tool names or `*`-patterns to
19
+ * exposure words; the pi alias `codemode-deferred` normalizes to
20
+ * `codemode`. A server whose tools all resolve `hidden` contributes
21
+ * nothing scripts can call (hidden tools never register), so it is
22
+ * excluded from the snapshot: that happens when the server default is
23
+ * `hidden` and every override is `hidden` too (an exact name wins over
24
+ * patterns; among patterns the first match in config order wins — so
25
+ * only a fully-hidden override table leaves every tool hidden).
26
+ *
27
+ * Snapshot semantics (the spec ruling): the config is read ONCE at factory
28
+ * time; late or flapping servers render un-namespaced in the section until
29
+ * the next boot. The section BODY re-renders live from
30
+ * `PromptInput.agent.callable` each turn (upstream's render), so tool
31
+ * lists are never stale — only the namespace GROUPING is a snapshot.
32
+ */
33
+ import type { CodemodeNamespace } from "@earendil-works/pi-durable/codemode";
34
+ /** The env override the config store reads (sigrun-ext-mcp's name). */
35
+ export declare const MCP_CONFIG_ENV = "SIGRUN_MCP_CONFIG";
36
+ /** The exposure words, after normalization (mcp repo's McpExposure). */
37
+ declare const EXPOSURE_WORDS: readonly ["codemode", "deferred", "direct", "hidden"];
38
+ type ExposureWord = (typeof EXPOSURE_WORDS)[number];
39
+ /** One server entry as this package needs it (a subset of the mcp repo's
40
+ * McpServerConfig: only the namespace-relevant fields). */
41
+ export interface McpServerEntry {
42
+ /** Skip this server entirely (either false means off). */
43
+ readonly enabled?: boolean;
44
+ readonly disabled?: boolean;
45
+ /** Server-default exposure (default "codemode"). */
46
+ readonly exposure?: ExposureWord;
47
+ /** Per-tool overrides: exact names beat `*`-patterns; first pattern wins. */
48
+ readonly toolExposure?: Readonly<Record<string, ExposureWord>>;
49
+ }
50
+ /** Resolve the config path without reading it (for diagnostics). */
51
+ export declare function resolveMcpConfigPath(env?: NodeJS.ProcessEnv): string;
52
+ /** Whether a configured server participates at all (mcp repo's
53
+ * `serverEnabled`): `enabled: false` and `disabled: true` both skip it. */
54
+ export declare function serverEnabled(server: McpServerEntry): boolean;
55
+ /**
56
+ * Read the mcp.json from the resolved path and build the namespace
57
+ * snapshot: one `CodemodeNamespace` per participating server with prefix
58
+ * `"<server>/"` (the namespace ruler: our tool-name shape is already
59
+ * `<server>/<tool>`, so upstream's longest-prefix grouping needs no renames).
60
+ *
61
+ * A missing file is an EMPTY namespace list (the extension still ships its
62
+ * codemode tool + section). An existing-but-bad file or entry warns at
63
+ * factory time (the spec's best-effort boot log; NOT the contract — a bad
64
+ * config must not cost the model its codemode tool) and the extension
65
+ * installs un-namespaced rather than throwing.
66
+ */
67
+ export declare function loadNamespaces(env?: NodeJS.ProcessEnv): readonly CodemodeNamespace[];
68
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * The codemode config snapshot: the mcp.json-derived namespace list.
3
+ *
4
+ * Reuses the MCP config RESOLUTION and the namespace-relevant parse SHAPES
5
+ * from sigrun-ext-mcp (rev-3 ruling accepts the duplicated parse of the
6
+ * config file across repos; sigrun-ext-mcp is not npm-published, so its
7
+ * `parseMcpConfig` cannot be imported here — the shapes are duplicated
8
+ * with the derivations quoted below for review parity).
9
+ *
10
+ * Resolution order (identical to sigrun-ext-mcp src/config.ts):
11
+ * 1. `SIGRUN_MCP_CONFIG` env override — an explicit path to an mcp.json
12
+ * 2. `~/.config/sigrun/mcp.json` (the sigrun config dir)
13
+ *
14
+ * What we read from each server entry:
15
+ * - `serverEnabled` (mcp repo's derivation): `enabled: false` OR
16
+ * `disabled: true` skips the server entirely.
17
+ * - Exposure (mcp repo's shape): a server-default `exposure` word, plus
18
+ * `toolExposure` overrides mapping exact tool names or `*`-patterns to
19
+ * exposure words; the pi alias `codemode-deferred` normalizes to
20
+ * `codemode`. A server whose tools all resolve `hidden` contributes
21
+ * nothing scripts can call (hidden tools never register), so it is
22
+ * excluded from the snapshot: that happens when the server default is
23
+ * `hidden` and every override is `hidden` too (an exact name wins over
24
+ * patterns; among patterns the first match in config order wins — so
25
+ * only a fully-hidden override table leaves every tool hidden).
26
+ *
27
+ * Snapshot semantics (the spec ruling): the config is read ONCE at factory
28
+ * time; late or flapping servers render un-namespaced in the section until
29
+ * the next boot. The section BODY re-renders live from
30
+ * `PromptInput.agent.callable` each turn (upstream's render), so tool
31
+ * lists are never stale — only the namespace GROUPING is a snapshot.
32
+ */
33
+ import { existsSync, readFileSync } from "node:fs";
34
+ import { homedir } from "node:os";
35
+ import { isAbsolute, resolve } from "node:path";
36
+ /** The env override the config store reads (sigrun-ext-mcp's name). */
37
+ export const MCP_CONFIG_ENV = "SIGRUN_MCP_CONFIG";
38
+ /** The exposure words, after normalization (mcp repo's McpExposure). */
39
+ const EXPOSURE_WORDS = ["codemode", "deferred", "direct", "hidden"];
40
+ /** Parse one exposure word; the pi alias normalizes to "codemode". */
41
+ const EXPOSURE_WORD_SET = new Set(EXPOSURE_WORDS);
42
+ function parseExposure(value, where) {
43
+ if (value === "codemode-deferred")
44
+ return "codemode";
45
+ if (typeof value !== "string" || !EXPOSURE_WORD_SET.has(value)) {
46
+ throw new Error(`${where} exposure must be one of ${EXPOSURE_WORDS.map((word) => JSON.stringify(word)).join(", ")} (or "codemode-deferred", the pi alias for "codemode")`);
47
+ }
48
+ return value;
49
+ }
50
+ /** Resolve the config path without reading it (for diagnostics). */
51
+ export function resolveMcpConfigPath(env = process.env) {
52
+ const override = env[MCP_CONFIG_ENV];
53
+ if (override !== undefined && override !== "") {
54
+ // A relative override resolves against cwd, like sigrun's other paths.
55
+ return isAbsolute(override) ? override : resolve(override);
56
+ }
57
+ return resolve(homedir(), ".config", "sigrun", "mcp.json");
58
+ }
59
+ function isRecord(value) {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+ /** Whether a configured server participates at all (mcp repo's
63
+ * `serverEnabled`): `enabled: false` and `disabled: true` both skip it. */
64
+ export function serverEnabled(server) {
65
+ return server.enabled !== false && server.disabled !== true;
66
+ }
67
+ /** Whether every tool of this server resolves `hidden`: the server default
68
+ * is `hidden` and no override can un-hide a tool (every override value is
69
+ * `hidden`; an exact name or pattern pointing elsewhere un-hides whatever
70
+ * it matches). Hidden tools never register, so such a server contributes
71
+ * nothing scripts can call and gets no namespace. */
72
+ function serverFullyHidden(server) {
73
+ if ((server.exposure ?? "codemode") !== "hidden")
74
+ return false;
75
+ return Object.values(server.toolExposure ?? {}).every((word) => word === "hidden");
76
+ }
77
+ /**
78
+ * Read the mcp.json from the resolved path and build the namespace
79
+ * snapshot: one `CodemodeNamespace` per participating server with prefix
80
+ * `"<server>/"` (the namespace ruler: our tool-name shape is already
81
+ * `<server>/<tool>`, so upstream's longest-prefix grouping needs no renames).
82
+ *
83
+ * A missing file is an EMPTY namespace list (the extension still ships its
84
+ * codemode tool + section). An existing-but-bad file or entry warns at
85
+ * factory time (the spec's best-effort boot log; NOT the contract — a bad
86
+ * config must not cost the model its codemode tool) and the extension
87
+ * installs un-namespaced rather than throwing.
88
+ */
89
+ export function loadNamespaces(env = process.env) {
90
+ const path = resolveMcpConfigPath(env);
91
+ if (!existsSync(path))
92
+ return [];
93
+ try {
94
+ return parseNamespaces(readFileSync(path, "utf8"), path);
95
+ }
96
+ catch (cause) {
97
+ console.warn(`sigrun-ext-codemode: mcp config ${path} is invalid (codemode ` +
98
+ `tools install un-namespaced): ${cause instanceof Error ? cause.message : String(cause)}`);
99
+ return [];
100
+ }
101
+ }
102
+ /** Parse and validate the config body into the namespace snapshot. */
103
+ function parseNamespaces(text, path) {
104
+ const parsed = JSON.parse(text);
105
+ if (!isRecord(parsed) || !isRecord(parsed.servers)) {
106
+ throw new Error(`must be a JSON object with a "servers" object mapping names to server entries`);
107
+ }
108
+ const namespaces = [];
109
+ for (const [name, entry] of Object.entries(parsed.servers)) {
110
+ if (!isRecord(entry)) {
111
+ throw new Error(`server "${name}" must be an object`);
112
+ }
113
+ const server = parseServerEntry(entry, path, name);
114
+ if (!serverEnabled(server))
115
+ continue;
116
+ if (serverFullyHidden(server)) {
117
+ // The spec's best-effort boot log: hidden-only rows.
118
+ console.warn(`sigrun-ext-codemode: mcp server "${name}" resolves fully ` +
119
+ `hidden (excluded from the codemode namespaces)`);
120
+ continue;
121
+ }
122
+ namespaces.push({
123
+ name,
124
+ description: `MCP server ${name} (tools ${name}/<tool>, registered by sigrun-ext-mcp)`,
125
+ prefix: `${name}/`,
126
+ });
127
+ }
128
+ return namespaces;
129
+ }
130
+ /** Validate one server entry's namespace-relevant fields. */
131
+ function parseServerEntry(entry, path, name) {
132
+ for (const field of ["enabled", "disabled"]) {
133
+ const value = entry[field];
134
+ if (value !== undefined && typeof value !== "boolean") {
135
+ throw new Error(`mcp config ${path}: server "${name}" ${field} must be a boolean`);
136
+ }
137
+ }
138
+ return {
139
+ ...(entry.enabled === undefined
140
+ ? {}
141
+ : { enabled: entry.enabled }),
142
+ ...(entry.disabled === undefined
143
+ ? {}
144
+ : { disabled: entry.disabled }),
145
+ ...(entry.exposure === undefined
146
+ ? {}
147
+ : {
148
+ exposure: parseExposure(entry.exposure, `mcp config ${path}: server "${name}"`),
149
+ }),
150
+ ...(entry.toolExposure === undefined
151
+ ? {}
152
+ : {
153
+ toolExposure: parseToolExposure(entry.toolExposure, path, name),
154
+ }),
155
+ };
156
+ }
157
+ function parseToolExposure(value, path, name) {
158
+ if (!isRecord(value)) {
159
+ throw new Error(`mcp config ${path}: server "${name}" toolExposure must be an object`);
160
+ }
161
+ const overrides = {};
162
+ for (const [key, exposure] of Object.entries(value)) {
163
+ overrides[key] = parseExposure(exposure, `mcp config ${path}: server "${name}" toolExposure "${key}"`);
164
+ }
165
+ return overrides;
166
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The codemode extension wrapper (sigrun-ext-codemode): installs upstream's
3
+ * `codemode` extension (the `codemode` tool, named exactly, with
4
+ * `callers: ["model"]`, plus the upstream `codemode` prompt section) with
5
+ * our two sigrun additions:
6
+ *
7
+ * 1. The MCP namespace snapshot (src/config.ts): one `CodemodeNamespace`
8
+ * per configured MCP server, prefix `"<server>/"`, read once at factory
9
+ * time. The upstream section groups `agent.callable` tools by the
10
+ * longest matching prefix, so sigrun's `<server>/<tool>` MCP tools
11
+ * render grouped per server with no renames (the namespace ruler).
12
+ *
13
+ * 2. The warn-if-not-offered channel (the spec's corrected mechanism):
14
+ * when the extension is installed but the `codemode` TOOL is not
15
+ * model-offered in this request, a short companion section renders the
16
+ * enable notice instead of silence. Upstream's `codemode` section
17
+ * renders `undefined` when nothing is callable, and it does NOT know
18
+ * whether the codemode tool itself is offered — a model whose
19
+ * Settings → default tools excluded `codemode` would see MCP tools
20
+ * listed as script-callable with no way to learn the tool is off. The
21
+ * companion section renders NOTHING (returns undefined, so no tag is
22
+ * written) whenever the tool IS offered, which de-duplicates with
23
+ * upstream's listing by construction.
24
+ *
25
+ * Mechanism choice (documented for review): a `wrapSection("codemode", …)`
26
+ * would also work — pi-durable's `resolveAgent` applies wraps across
27
+ * selected extensions, and our factory owns both the wrapped section and
28
+ * the wrapper — but the wrap could only prepend text around upstream's
29
+ * render, and upstream's render already covers the offered case; the
30
+ * companion section keys the notice to the not-offered condition exactly,
31
+ * leaves the upstream render untouched, and renders undefined (absent from
32
+ * the prompt, no `<key>` tag written) when offered. Companion section it
33
+ * is. Both extensions are installed by the same factory, so the pair
34
+ * (upstream section + notice section) travels together.
35
+ */
36
+ import { type CodemodeNamespace } from "@earendil-works/pi-durable/codemode";
37
+ import type { CodemodeSandboxBase } from "@earendil-works/pi-codemode/portable";
38
+ import { type Extension, type PromptInput } from "@earendil-works/pi-durable";
39
+ /** Our companion section's key (distinct from upstream's `"codemode"`). */
40
+ export declare const CODEMODE_NOTICE_SECTION_KEY = "codemode_notice";
41
+ /** The enable notice, the spec's wording. */
42
+ export declare const CODEMODE_NOT_OFFERED_NOTICE: string;
43
+ /**
44
+ * The companion section render: the notice ONLY when the codemode tool
45
+ * is installed (one of the selected extensions registers it) yet not
46
+ * model-offered in this request (`agent.tools` is the offered set);
47
+ * `undefined` otherwise (offered, or not installed — nothing to warn
48
+ * about). Installation cannot be read from `agent.callable`: that set
49
+ * holds only tools-callable tools, and `codemode` registers with
50
+ * `callers: ["model"]`.
51
+ */
52
+ export declare function renderCodemodeNotice(input: PromptInput): string | undefined;
53
+ /**
54
+ * The sigrun codemode extension: upstream's `codemode` extension (tool +
55
+ * section, with our namespace snapshot) plus the companion notice section.
56
+ */
57
+ export declare function createSigrunCodemodeExtension(options: {
58
+ /** Runs the scripts: `CodemodeSandbox` on Node. */
59
+ readonly sandbox: CodemodeSandboxBase;
60
+ /** The factory-time MCP namespace snapshot (src/config.ts). */
61
+ readonly namespaces?: readonly CodemodeNamespace[];
62
+ }): Extension;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The codemode extension wrapper (sigrun-ext-codemode): installs upstream's
3
+ * `codemode` extension (the `codemode` tool, named exactly, with
4
+ * `callers: ["model"]`, plus the upstream `codemode` prompt section) with
5
+ * our two sigrun additions:
6
+ *
7
+ * 1. The MCP namespace snapshot (src/config.ts): one `CodemodeNamespace`
8
+ * per configured MCP server, prefix `"<server>/"`, read once at factory
9
+ * time. The upstream section groups `agent.callable` tools by the
10
+ * longest matching prefix, so sigrun's `<server>/<tool>` MCP tools
11
+ * render grouped per server with no renames (the namespace ruler).
12
+ *
13
+ * 2. The warn-if-not-offered channel (the spec's corrected mechanism):
14
+ * when the extension is installed but the `codemode` TOOL is not
15
+ * model-offered in this request, a short companion section renders the
16
+ * enable notice instead of silence. Upstream's `codemode` section
17
+ * renders `undefined` when nothing is callable, and it does NOT know
18
+ * whether the codemode tool itself is offered — a model whose
19
+ * Settings → default tools excluded `codemode` would see MCP tools
20
+ * listed as script-callable with no way to learn the tool is off. The
21
+ * companion section renders NOTHING (returns undefined, so no tag is
22
+ * written) whenever the tool IS offered, which de-duplicates with
23
+ * upstream's listing by construction.
24
+ *
25
+ * Mechanism choice (documented for review): a `wrapSection("codemode", …)`
26
+ * would also work — pi-durable's `resolveAgent` applies wraps across
27
+ * selected extensions, and our factory owns both the wrapped section and
28
+ * the wrapper — but the wrap could only prepend text around upstream's
29
+ * render, and upstream's render already covers the offered case; the
30
+ * companion section keys the notice to the not-offered condition exactly,
31
+ * leaves the upstream render untouched, and renders undefined (absent from
32
+ * the prompt, no `<key>` tag written) when offered. Companion section it
33
+ * is. Both extensions are installed by the same factory, so the pair
34
+ * (upstream section + notice section) travels together.
35
+ */
36
+ import { CODEMODE_TOOL_NAME, createCodemodeExtension, } from "@earendil-works/pi-durable/codemode";
37
+ import { defineExtension, } from "@earendil-works/pi-durable";
38
+ /** Our companion section's key (distinct from upstream's `"codemode"`). */
39
+ export const CODEMODE_NOTICE_SECTION_KEY = "codemode_notice";
40
+ /** The enable notice, the spec's wording. */
41
+ export const CODEMODE_NOT_OFFERED_NOTICE = "codemode scripts are available but the tool is not offered; enable it " +
42
+ "via Settings → default tools";
43
+ /**
44
+ * The companion section render: the notice ONLY when the codemode tool
45
+ * is installed (one of the selected extensions registers it) yet not
46
+ * model-offered in this request (`agent.tools` is the offered set);
47
+ * `undefined` otherwise (offered, or not installed — nothing to warn
48
+ * about). Installation cannot be read from `agent.callable`: that set
49
+ * holds only tools-callable tools, and `codemode` registers with
50
+ * `callers: ["model"]`.
51
+ */
52
+ export function renderCodemodeNotice(input) {
53
+ const installed = input.agent.extensions.some((extension) => (extension.tools ?? []).some((tool) => tool.name === CODEMODE_TOOL_NAME));
54
+ if (!installed)
55
+ return undefined; // not installed: no lie to correct
56
+ const offered = input.agent.tools.some((tool) => tool.name === CODEMODE_TOOL_NAME);
57
+ return offered ? undefined : CODEMODE_NOT_OFFERED_NOTICE;
58
+ }
59
+ /**
60
+ * The sigrun codemode extension: upstream's `codemode` extension (tool +
61
+ * section, with our namespace snapshot) plus the companion notice section.
62
+ */
63
+ export function createSigrunCodemodeExtension(options) {
64
+ const upstream = createCodemodeExtension({
65
+ sandbox: options.sandbox,
66
+ ...(options.namespaces === undefined
67
+ ? {}
68
+ : { namespaces: options.namespaces }),
69
+ });
70
+ return defineExtension({
71
+ ...upstream,
72
+ sections: [
73
+ ...(upstream.sections ?? []),
74
+ {
75
+ key: CODEMODE_NOTICE_SECTION_KEY,
76
+ render: renderCodemodeNotice,
77
+ },
78
+ ],
79
+ });
80
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * sigrun-ext-codemode: the codemode factory (the loader contract).
3
+ *
4
+ * The sigrun extension-package contract (sigrun's src/harness/loader.ts):
5
+ * the default export is a FACTORY TAKING NO ARGUMENTS, returning
6
+ * `{ extension }`. The loader calls it synchronously at boot, before
7
+ * `Harness.open` reads the registry.
8
+ *
9
+ * What the factory does, in order:
10
+ * 1. Reads the MCP namespace snapshot ONCE (src/config.ts):
11
+ * `SIGRUN_MCP_CONFIG` → `~/.config/sigrun/mcp.json`; absent file →
12
+ * empty namespaces (the extension still ships its codemode tool +
13
+ * section); a bad file warns and installs un-namespaced.
14
+ * 2. Builds the QuickJS sandbox with the pinned constructor options
15
+ * (timeoutMs 300_000 — pi-codemode's own default, restated so the
16
+ * contract does not drift; memoryLimitBytes 256 MiB — a real cap
17
+ * where the upstream default is unbounded wasm32).
18
+ * 3. Wraps upstream's `createCodemodeExtension({ sandbox, namespaces })`
19
+ * with the companion notice section (src/extension.ts).
20
+ *
21
+ * Note on `timeoutMs` (verified against pi-codemode 1.2.0): the
22
+ * CONSTRUCTOR timeout governs `sandbox.execute()`; the durable codemode
23
+ * tool path (`sandbox.start()`) takes its deadline from the script's own
24
+ * `// @options: {"timeout_ms": N}` line (no options line → no deadline:
25
+ * the sandbox default does not apply there). The constructor cap is
26
+ * still the right pin: it bounds every execution of the plain sandbox
27
+ * surface and documents the intended ceiling.
28
+ *
29
+ * `quickjs-wasi` is a REAL dependency of this package, not a transitive
30
+ * courtesy: pi-codemode's worker resolves `quickjs-wasi/quickjs.wasm`
31
+ * from disk at the first script. A re-pinned or deduped-away transitive
32
+ * copy strands the wasm file and the first codemode call fails at runtime
33
+ * (500s in prod) — declaring it here pins the resolution to this
34
+ * package's own tree.
35
+ */
36
+ import type { CodemodeSandboxBase } from "@earendil-works/pi-codemode/portable";
37
+ import type { CodemodeNamespace } from "@earendil-works/pi-durable/codemode";
38
+ import type { Extension } from "@earendil-works/pi-durable";
39
+ /** Pinned per-execution wall-clock deadline (pi-codemode's default). */
40
+ export declare const CODEMODE_TIMEOUT_MS = 300000;
41
+ /** Pinned QuickJS heap cap (256 MiB; upstream default is unbounded). */
42
+ export declare const CODEMODE_MEMORY_LIMIT_BYTES: number;
43
+ /** What {@link createCodemodeWiring} returns (the loader contract). */
44
+ export interface CodemodeWiring {
45
+ /** The extension to install into the harness registry. */
46
+ readonly extension: Extension;
47
+ /** The sandbox the extension runs scripts in (call `close()` on shutdown). */
48
+ readonly sandbox: CodemodeSandboxBase;
49
+ /** The factory-time namespace snapshot (empty without an mcp.json). */
50
+ readonly namespaces: readonly CodemodeNamespace[];
51
+ /**
52
+ * Close the sandbox: aborts in-flight executions and stops the worker
53
+ * machinery. Called by hosts on harness shutdown; a fresh factory builds
54
+ * a fresh sandbox, so a closed wiring is never reused.
55
+ */
56
+ close(): Promise<void>;
57
+ }
58
+ /**
59
+ * The codemode factory (the loader contract: no arguments; config resolves
60
+ * from the package context). Each call builds a FRESH sandbox — one
61
+ * wiring, one sandbox, no shared worker state across installs.
62
+ */
63
+ export declare function createCodemodeWiring(): CodemodeWiring;
64
+ /**
65
+ * The sigrun extension-package contract: the default export is a factory
66
+ * taking no arguments, returning `{ extension }`.
67
+ */
68
+ declare const _default: typeof createCodemodeWiring;
69
+ export default _default;
70
+ export { CODEMODE_NOT_OFFERED_NOTICE, CODEMODE_NOTICE_SECTION_KEY, renderCodemodeNotice, } from "./extension.js";
71
+ export { loadNamespaces, MCP_CONFIG_ENV, resolveMcpConfigPath, serverEnabled, } from "./config.js";
72
+ export type { McpServerEntry } from "./config.js";
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * sigrun-ext-codemode: the codemode factory (the loader contract).
3
+ *
4
+ * The sigrun extension-package contract (sigrun's src/harness/loader.ts):
5
+ * the default export is a FACTORY TAKING NO ARGUMENTS, returning
6
+ * `{ extension }`. The loader calls it synchronously at boot, before
7
+ * `Harness.open` reads the registry.
8
+ *
9
+ * What the factory does, in order:
10
+ * 1. Reads the MCP namespace snapshot ONCE (src/config.ts):
11
+ * `SIGRUN_MCP_CONFIG` → `~/.config/sigrun/mcp.json`; absent file →
12
+ * empty namespaces (the extension still ships its codemode tool +
13
+ * section); a bad file warns and installs un-namespaced.
14
+ * 2. Builds the QuickJS sandbox with the pinned constructor options
15
+ * (timeoutMs 300_000 — pi-codemode's own default, restated so the
16
+ * contract does not drift; memoryLimitBytes 256 MiB — a real cap
17
+ * where the upstream default is unbounded wasm32).
18
+ * 3. Wraps upstream's `createCodemodeExtension({ sandbox, namespaces })`
19
+ * with the companion notice section (src/extension.ts).
20
+ *
21
+ * Note on `timeoutMs` (verified against pi-codemode 1.2.0): the
22
+ * CONSTRUCTOR timeout governs `sandbox.execute()`; the durable codemode
23
+ * tool path (`sandbox.start()`) takes its deadline from the script's own
24
+ * `// @options: {"timeout_ms": N}` line (no options line → no deadline:
25
+ * the sandbox default does not apply there). The constructor cap is
26
+ * still the right pin: it bounds every execution of the plain sandbox
27
+ * surface and documents the intended ceiling.
28
+ *
29
+ * `quickjs-wasi` is a REAL dependency of this package, not a transitive
30
+ * courtesy: pi-codemode's worker resolves `quickjs-wasi/quickjs.wasm`
31
+ * from disk at the first script. A re-pinned or deduped-away transitive
32
+ * copy strands the wasm file and the first codemode call fails at runtime
33
+ * (500s in prod) — declaring it here pins the resolution to this
34
+ * package's own tree.
35
+ */
36
+ import { CodemodeSandbox } from "@earendil-works/pi-codemode";
37
+ import { loadNamespaces } from "./config.js";
38
+ import { createSigrunCodemodeExtension } from "./extension.js";
39
+ /** Pinned per-execution wall-clock deadline (pi-codemode's default). */
40
+ export const CODEMODE_TIMEOUT_MS = 300_000;
41
+ /** Pinned QuickJS heap cap (256 MiB; upstream default is unbounded). */
42
+ export const CODEMODE_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;
43
+ /**
44
+ * The codemode factory (the loader contract: no arguments; config resolves
45
+ * from the package context). Each call builds a FRESH sandbox — one
46
+ * wiring, one sandbox, no shared worker state across installs.
47
+ */
48
+ export function createCodemodeWiring() {
49
+ const namespaces = loadNamespaces();
50
+ const sandbox = new CodemodeSandbox({
51
+ timeoutMs: CODEMODE_TIMEOUT_MS,
52
+ memoryLimitBytes: CODEMODE_MEMORY_LIMIT_BYTES,
53
+ });
54
+ const extension = createSigrunCodemodeExtension({ sandbox, namespaces });
55
+ return {
56
+ extension,
57
+ sandbox,
58
+ namespaces,
59
+ close: () => sandbox.close(),
60
+ };
61
+ }
62
+ /**
63
+ * The sigrun extension-package contract: the default export is a factory
64
+ * taking no arguments, returning `{ extension }`.
65
+ */
66
+ export default createCodemodeWiring;
67
+ export { CODEMODE_NOT_OFFERED_NOTICE, CODEMODE_NOTICE_SECTION_KEY, renderCodemodeNotice, } from "./extension.js";
68
+ export { loadNamespaces, MCP_CONFIG_ENV, resolveMcpConfigPath, serverEnabled, } from "./config.js";
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
- "name": "@sigrun/ext-codemode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@sigrun/ext-codemode",
3
+ "version": "0.1.1",
4
+ "description": "Codemode extension for sigrun (pi-durable): the `codemode` tool \u2014 model-written JavaScript in a QuickJS sandbox, with MCP-server tools namespaced as script-callable tools",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node scripts/check-manifest.mjs --expect-name @sigrun/ext-codemode && vitest run",
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "dependencies": {
28
+ "quickjs-wasi": "3.6.2"
29
+ },
30
+ "peerDependencies": {
31
+ "@earendil-works/chord": "1.3.0",
32
+ "@earendil-works/pi-ai": "1.3.0",
33
+ "@earendil-works/pi-codemode": "1.3.0",
34
+ "@earendil-works/pi-durable": "1.3.0"
35
+ },
36
+ "devDependencies": {
37
+ "@earendil-works/chord": "1.3.0",
38
+ "@earendil-works/pi-ai": "1.3.0",
39
+ "@earendil-works/pi-codemode": "1.3.0",
40
+ "@earendil-works/pi-durable": "1.3.0",
41
+ "@types/node": "^26.6.4",
42
+ "tsx": "^4.23.15",
43
+ "typescript": "^7.0.2",
44
+ "vitest": "^5.0.3"
45
+ },
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "github",
49
+ "url": "git+https://github.com/sigrun-ai/sigrun-ext-codemode.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/sigrun-ai/sigrun-ext-codemode/issues"
53
+ },
54
+ "homepage": "https://github.com/sigrun-ai/sigrun-ext-codemode#readme",
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "provenance": true
58
+ }
59
+ }