open-multi-agent-kit 1.2.2 → 1.2.3

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.2.3] - 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - Verified-run suite hardening: tests use a 15s cleanup budget so slow namespace teardown on ubuntu-22.04 runners drains before settling, matching the witness contract without weakening fail-closed outcomes (no product-code change).
10
+
3
11
  ## [1.2.2] - 2026-09-23
4
12
 
5
13
  ### Fixed
package/README.md CHANGED
@@ -47,7 +47,7 @@ OMK stores user configuration under `~/.omk/agent/` and project configuration un
47
47
  Install `omk-book-to-skill` to compile documents into reusable skills without adding Python extractors to OMK core:
48
48
 
49
49
  ```bash
50
- omk install npm:omk-book-to-skill@1.2.2
50
+ omk install npm:omk-book-to-skill@1.2.3
51
51
  ```
52
52
 
53
53
  It provides compile, update, and verification commands plus a local SHA-256 provenance manifest. See [Book to Skill](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/book-to-skill.md).
@@ -258,7 +258,7 @@ npm test
258
258
 
259
259
  See [Development](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/development.md) and [CONTRIBUTING.md](https://github.com/dmae97/omk/blob/main/CONTRIBUTING.md).
260
260
 
261
- Release notes: [v1.2.2](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v1.2.2.md).
261
+ Release notes: [v1.2.3](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v1.2.3.md).
262
262
 
263
263
  ## License
264
264
 
@@ -72,6 +72,10 @@ export declare function probeOwnedNamespace(identity: NamespaceIdentity): "termi
72
72
  * probe is inconclusive, and that window resolves to `gone` once the kernel
73
73
  * finishes teardown. Only a probe that is still inconclusive at the deadline
74
74
  * stays `unknown`; a still-alive init is `populated`.
75
+ *
76
+ * `drained` means the kernel has provably killed every member; a task frozen
77
+ * in uninterruptible kernel sleep can linger in /proc briefly afterwards —
78
+ * that is kernel bookkeeping, not a live boundary.
75
79
  */
76
80
  export declare function awaitBoundaryDrained(identity: NamespaceIdentity, budgetMs: number): Promise<"drained" | "populated" | "unknown">;
77
81
  //# sourceMappingURL=supervisor-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/verified-run/supervisor-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,iBAAiB,EAAkB,MAAM,yBAAyB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,eAAO,MAAM,sBAAsB,iWA+BzB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,OAAO,CAAA;CAAE,CAAC;AAErF;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAIrG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAiC5F;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAGnG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACzC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC,CAQ9C","sourcesContent":["import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { type NamespaceIdentity, probeNamespace } from \"./namespace-identity.ts\";\nimport { VerifiedRunError } from \"./storage.ts\";\n\n/**\n * Owned-process supervisor boundary (WP02).\n *\n * Three concerns are separated here so the broker only orchestrates them:\n *\n * - spawn: `SUPERVISOR_SYSTEM_ARGS` + `loadSupervisorBackend` describe the only\n * backend this package trusts — bubblewrap with `--unshare-all`, which puts\n * the supervised command in a fresh PID namespace whose init is the only\n * process the host can see. A platform without that backend has no way to\n * prove termination, so callers must refuse with `unsupported_boundary`\n * instead of degrading to best-effort signaling.\n * - cancel: `escalateTermination` sends SIGKILL to the supervised direct\n * child (the bwrap supervisor). `--die-with-parent` propagates death to the\n * namespace init, and the kernel then SIGKILLs every remaining task in that\n * namespace — including non-cooperative workers and detached (`setsid`)\n * descendants. Signal delivery is never treated as termination.\n * - termination witness: `probeNamespace` detects the recorded namespace\n * init's death and `awaitBoundaryDrained` waits for it. When a PID\n * namespace's init exits, the kernel SIGKILLs every remaining task in that\n * namespace, so init death is the exact termination witness — a `close`\n * event, a resolved promise, or a dead direct child with surviving\n * descendants is not. `namespaceMemberPids` remains as a diagnostic\n * enumeration only: host process-table scans are environment-dependent\n * (zombies keep their ns link until reaped; foreign same-uid tasks can be\n * unreadable) and have poisoned drains on kernels where either occurs.\n */\nexport const SUPERVISOR_BINARY = \"/usr/bin/bwrap\";\n\nexport const SUPERVISOR_SYSTEM_ARGS = [\n\t\"--unshare-all\",\n\t\"--die-with-parent\",\n\t\"--new-session\",\n\t\"--cap-drop\",\n\t\"ALL\",\n\t\"--clearenv\",\n\t\"--ro-bind\",\n\t\"/usr\",\n\t\"/usr\",\n\t\"--symlink\",\n\t\"usr/bin\",\n\t\"/bin\",\n\t\"--symlink\",\n\t\"usr/lib\",\n\t\"/lib\",\n\t\"--symlink\",\n\t\"usr/lib64\",\n\t\"/lib64\",\n\t\"--proc\",\n\t\"/proc\",\n\t\"--dev\",\n\t\"/dev\",\n\t\"--tmpfs\",\n\t\"/tmp\",\n\t\"--setenv\",\n\t\"PATH\",\n\t\"/usr/bin:/bin\",\n\t\"--setenv\",\n\t\"LANG\",\n\t\"C.UTF-8\",\n] as const;\n\nexport type SupervisorChild = { readonly kill: (signal: NodeJS.Signals) => boolean };\n\n/**\n * The only termination guarantee this package honors: a private PID namespace\n * plus the Linux bwrap supervisor. Anything else is refused at the boundary.\n */\nexport function loadSupervisorBackend(): { readonly binary: string; readonly args: readonly string[] } {\n\tif (process.platform !== \"linux\" || !existsSync(SUPERVISOR_BINARY))\n\t\tthrow new VerifiedRunError(\"unsupported_boundary\");\n\treturn { binary: SUPERVISOR_BINARY, args: SUPERVISOR_SYSTEM_ARGS };\n}\n\n/**\n * SIGKILL escalation to the supervised direct child. `--die-with-parent` makes\n * the kernel kill the whole PID namespace once this supervisor dies, so no\n * per-descendant signaling (which PID reuse would make unsafe) is needed.\n */\nexport function escalateTermination(child: SupervisorChild): void {\n\tchild.kill(\"SIGKILL\");\n}\n\n/**\n * Enumerate host PIDs of every task still inside `identity.namespace`.\n *\n * Membership is keyed on the recorded namespace symlink, not on the init's\n * PID: a fabricated or reaped direct child cannot mask surviving descendants.\n *\n * A task whose namespace link cannot be read is only a candidate member when\n * it could belong to the recorded boundary at all. The backend's members\n * always run under our real uid — the pid namespace is owned by a user\n * namespace we created and `CAP_SETUID` is dropped — so a task owned by\n * another uid, or one that vanished mid-scan, cannot be a member and is\n * skipped. An unreadable task owned by our uid is conservatively counted:\n * failing to prove emptiness must fail closed.\n */\nexport function namespaceMemberPids(identity: Pick<NamespaceIdentity, \"namespace\">): number[] {\n\tconst members: number[] = [];\n\t// A platform without getuid cannot exclude foreign-owned tasks, so every\n\t// unreadable entry stays a candidate member (fail closed).\n\tconst uid = typeof process.getuid === \"function\" ? process.getuid() : undefined;\n\tfor (const entry of readdirSync(\"/proc\")) {\n\t\tif (!/^\\d+$/.test(entry)) continue;\n\t\ttry {\n\t\t\tif (readlinkSync(`/proc/${entry}/ns/pid`) !== identity.namespace) continue;\n\t\t\t// A zombie still carries the namespace link until reaped, but it\n\t\t\t// cannot run: counting it as a member confuses teardown with\n\t\t\t// survival.\n\t\t\tconst stat = readFileSync(`/proc/${entry}/stat`, \"utf8\");\n\t\t\tconst state = stat\n\t\t\t\t.slice(stat.lastIndexOf(\")\") + 2)\n\t\t\t\t.trim()\n\t\t\t\t.split(/\\s+/)[0];\n\t\t\tif (state !== \"Z\" && state !== \"X\") members.push(Number(entry));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error)) continue;\n\t\t\tif (error.code === \"ENOENT\" || error.code === \"ESRCH\") continue;\n\t\t\ttry {\n\t\t\t\tif (uid === undefined || statSync(`/proc/${entry}`).uid === uid) members.push(Number(entry));\n\t\t\t} catch (statError) {\n\t\t\t\tif (\n\t\t\t\t\t!(statError instanceof Error && \"code\" in statError) ||\n\t\t\t\t\t(statError.code !== \"ENOENT\" && statError.code !== \"ESRCH\")\n\t\t\t\t)\n\t\t\t\t\tmembers.push(Number(entry));\n\t\t\t}\n\t\t}\n\t}\n\treturn members;\n}\n\n/** Init death terminates the whole namespace; a zombie init already proves teardown finished. */\nexport function probeOwnedNamespace(identity: NamespaceIdentity): \"terminated\" | \"alive\" | \"unknown\" {\n\tconst status = probeNamespace(identity);\n\treturn status === \"gone\" ? \"terminated\" : status;\n}\n\n/**\n * Wait for the namespace init's death within budgetMs. Init death is the\n * termination witness: the kernel SIGKILLs every remaining task in the PID\n * namespace when its init exits, so a drained namespace needs no process-table\n * enumeration. `populated`/`unknown` never release claims.\n *\n * `unknown` is not terminal here: teardown has a transient window where the\n * init's stat is readable but its ns link read is denied or the identity\n * probe is inconclusive, and that window resolves to `gone` once the kernel\n * finishes teardown. Only a probe that is still inconclusive at the deadline\n * stays `unknown`; a still-alive init is `populated`.\n */\nexport async function awaitBoundaryDrained(\n\tidentity: NamespaceIdentity,\n\tbudgetMs: number,\n): Promise<\"drained\" | \"populated\" | \"unknown\"> {\n\tconst deadline = performance.now() + Math.max(0, budgetMs);\n\tfor (;;) {\n\t\tconst status = probeNamespace(identity);\n\t\tif (status === \"gone\") return \"drained\";\n\t\tif (performance.now() >= deadline) return status === \"alive\" ? \"populated\" : \"unknown\";\n\t\tawait delay(Math.min(10, Math.max(1, deadline - performance.now())));\n\t}\n}\n"]}
1
+ {"version":3,"file":"supervisor-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/verified-run/supervisor-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,iBAAiB,EAAkB,MAAM,yBAAyB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,eAAO,MAAM,sBAAsB,iWA+BzB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,OAAO,CAAA;CAAE,CAAC;AAErF;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAIrG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAiC5F;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAGnG;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACzC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC,CAQ9C","sourcesContent":["import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { type NamespaceIdentity, probeNamespace } from \"./namespace-identity.ts\";\nimport { VerifiedRunError } from \"./storage.ts\";\n\n/**\n * Owned-process supervisor boundary (WP02).\n *\n * Three concerns are separated here so the broker only orchestrates them:\n *\n * - spawn: `SUPERVISOR_SYSTEM_ARGS` + `loadSupervisorBackend` describe the only\n * backend this package trusts — bubblewrap with `--unshare-all`, which puts\n * the supervised command in a fresh PID namespace whose init is the only\n * process the host can see. A platform without that backend has no way to\n * prove termination, so callers must refuse with `unsupported_boundary`\n * instead of degrading to best-effort signaling.\n * - cancel: `escalateTermination` sends SIGKILL to the supervised direct\n * child (the bwrap supervisor). `--die-with-parent` propagates death to the\n * namespace init, and the kernel then SIGKILLs every remaining task in that\n * namespace — including non-cooperative workers and detached (`setsid`)\n * descendants. Signal delivery is never treated as termination.\n * - termination witness: `probeNamespace` detects the recorded namespace\n * init's death and `awaitBoundaryDrained` waits for it. When a PID\n * namespace's init exits, the kernel SIGKILLs every remaining task in that\n * namespace, so init death is the exact termination witness — a `close`\n * event, a resolved promise, or a dead direct child with surviving\n * descendants is not. `namespaceMemberPids` remains as a diagnostic\n * enumeration only: host process-table scans are environment-dependent\n * (zombies keep their ns link until reaped; foreign same-uid tasks can be\n * unreadable) and have poisoned drains on kernels where either occurs.\n */\nexport const SUPERVISOR_BINARY = \"/usr/bin/bwrap\";\n\nexport const SUPERVISOR_SYSTEM_ARGS = [\n\t\"--unshare-all\",\n\t\"--die-with-parent\",\n\t\"--new-session\",\n\t\"--cap-drop\",\n\t\"ALL\",\n\t\"--clearenv\",\n\t\"--ro-bind\",\n\t\"/usr\",\n\t\"/usr\",\n\t\"--symlink\",\n\t\"usr/bin\",\n\t\"/bin\",\n\t\"--symlink\",\n\t\"usr/lib\",\n\t\"/lib\",\n\t\"--symlink\",\n\t\"usr/lib64\",\n\t\"/lib64\",\n\t\"--proc\",\n\t\"/proc\",\n\t\"--dev\",\n\t\"/dev\",\n\t\"--tmpfs\",\n\t\"/tmp\",\n\t\"--setenv\",\n\t\"PATH\",\n\t\"/usr/bin:/bin\",\n\t\"--setenv\",\n\t\"LANG\",\n\t\"C.UTF-8\",\n] as const;\n\nexport type SupervisorChild = { readonly kill: (signal: NodeJS.Signals) => boolean };\n\n/**\n * The only termination guarantee this package honors: a private PID namespace\n * plus the Linux bwrap supervisor. Anything else is refused at the boundary.\n */\nexport function loadSupervisorBackend(): { readonly binary: string; readonly args: readonly string[] } {\n\tif (process.platform !== \"linux\" || !existsSync(SUPERVISOR_BINARY))\n\t\tthrow new VerifiedRunError(\"unsupported_boundary\");\n\treturn { binary: SUPERVISOR_BINARY, args: SUPERVISOR_SYSTEM_ARGS };\n}\n\n/**\n * SIGKILL escalation to the supervised direct child. `--die-with-parent` makes\n * the kernel kill the whole PID namespace once this supervisor dies, so no\n * per-descendant signaling (which PID reuse would make unsafe) is needed.\n */\nexport function escalateTermination(child: SupervisorChild): void {\n\tchild.kill(\"SIGKILL\");\n}\n\n/**\n * Enumerate host PIDs of every task still inside `identity.namespace`.\n *\n * Membership is keyed on the recorded namespace symlink, not on the init's\n * PID: a fabricated or reaped direct child cannot mask surviving descendants.\n *\n * A task whose namespace link cannot be read is only a candidate member when\n * it could belong to the recorded boundary at all. The backend's members\n * always run under our real uid — the pid namespace is owned by a user\n * namespace we created and `CAP_SETUID` is dropped — so a task owned by\n * another uid, or one that vanished mid-scan, cannot be a member and is\n * skipped. An unreadable task owned by our uid is conservatively counted:\n * failing to prove emptiness must fail closed.\n */\nexport function namespaceMemberPids(identity: Pick<NamespaceIdentity, \"namespace\">): number[] {\n\tconst members: number[] = [];\n\t// A platform without getuid cannot exclude foreign-owned tasks, so every\n\t// unreadable entry stays a candidate member (fail closed).\n\tconst uid = typeof process.getuid === \"function\" ? process.getuid() : undefined;\n\tfor (const entry of readdirSync(\"/proc\")) {\n\t\tif (!/^\\d+$/.test(entry)) continue;\n\t\ttry {\n\t\t\tif (readlinkSync(`/proc/${entry}/ns/pid`) !== identity.namespace) continue;\n\t\t\t// A zombie still carries the namespace link until reaped, but it\n\t\t\t// cannot run: counting it as a member confuses teardown with\n\t\t\t// survival.\n\t\t\tconst stat = readFileSync(`/proc/${entry}/stat`, \"utf8\");\n\t\t\tconst state = stat\n\t\t\t\t.slice(stat.lastIndexOf(\")\") + 2)\n\t\t\t\t.trim()\n\t\t\t\t.split(/\\s+/)[0];\n\t\t\tif (state !== \"Z\" && state !== \"X\") members.push(Number(entry));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error)) continue;\n\t\t\tif (error.code === \"ENOENT\" || error.code === \"ESRCH\") continue;\n\t\t\ttry {\n\t\t\t\tif (uid === undefined || statSync(`/proc/${entry}`).uid === uid) members.push(Number(entry));\n\t\t\t} catch (statError) {\n\t\t\t\tif (\n\t\t\t\t\t!(statError instanceof Error && \"code\" in statError) ||\n\t\t\t\t\t(statError.code !== \"ENOENT\" && statError.code !== \"ESRCH\")\n\t\t\t\t)\n\t\t\t\t\tmembers.push(Number(entry));\n\t\t\t}\n\t\t}\n\t}\n\treturn members;\n}\n\n/** Init death terminates the whole namespace; a zombie init already proves teardown finished. */\nexport function probeOwnedNamespace(identity: NamespaceIdentity): \"terminated\" | \"alive\" | \"unknown\" {\n\tconst status = probeNamespace(identity);\n\treturn status === \"gone\" ? \"terminated\" : status;\n}\n\n/**\n * Wait for the namespace init's death within budgetMs. Init death is the\n * termination witness: the kernel SIGKILLs every remaining task in the PID\n * namespace when its init exits, so a drained namespace needs no process-table\n * enumeration. `populated`/`unknown` never release claims.\n *\n * `unknown` is not terminal here: teardown has a transient window where the\n * init's stat is readable but its ns link read is denied or the identity\n * probe is inconclusive, and that window resolves to `gone` once the kernel\n * finishes teardown. Only a probe that is still inconclusive at the deadline\n * stays `unknown`; a still-alive init is `populated`.\n *\n * `drained` means the kernel has provably killed every member; a task frozen\n * in uninterruptible kernel sleep can linger in /proc briefly afterwards —\n * that is kernel bookkeeping, not a live boundary.\n */\nexport async function awaitBoundaryDrained(\n\tidentity: NamespaceIdentity,\n\tbudgetMs: number,\n): Promise<\"drained\" | \"populated\" | \"unknown\"> {\n\tconst deadline = performance.now() + Math.max(0, budgetMs);\n\tfor (;;) {\n\t\tconst status = probeNamespace(identity);\n\t\tif (status === \"gone\") return \"drained\";\n\t\tif (performance.now() >= deadline) return status === \"alive\" ? \"populated\" : \"unknown\";\n\t\tawait delay(Math.min(10, Math.max(1, deadline - performance.now())));\n\t}\n}\n"]}
@@ -149,6 +149,10 @@ export function probeOwnedNamespace(identity) {
149
149
  * probe is inconclusive, and that window resolves to `gone` once the kernel
150
150
  * finishes teardown. Only a probe that is still inconclusive at the deadline
151
151
  * stays `unknown`; a still-alive init is `populated`.
152
+ *
153
+ * `drained` means the kernel has provably killed every member; a task frozen
154
+ * in uninterruptible kernel sleep can linger in /proc briefly afterwards —
155
+ * that is kernel bookkeeping, not a live boundary.
152
156
  */
153
157
  export async function awaitBoundaryDrained(identity, budgetMs) {
154
158
  const deadline = performance.now() + Math.max(0, budgetMs);
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor-adapter.js","sourceRoot":"","sources":["../../../src/core/verified-run/supervisor-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { type NamespaceIdentity, probeNamespace } from \"./namespace-identity.ts\";\nimport { VerifiedRunError } from \"./storage.ts\";\n\n/**\n * Owned-process supervisor boundary (WP02).\n *\n * Three concerns are separated here so the broker only orchestrates them:\n *\n * - spawn: `SUPERVISOR_SYSTEM_ARGS` + `loadSupervisorBackend` describe the only\n * backend this package trusts — bubblewrap with `--unshare-all`, which puts\n * the supervised command in a fresh PID namespace whose init is the only\n * process the host can see. A platform without that backend has no way to\n * prove termination, so callers must refuse with `unsupported_boundary`\n * instead of degrading to best-effort signaling.\n * - cancel: `escalateTermination` sends SIGKILL to the supervised direct\n * child (the bwrap supervisor). `--die-with-parent` propagates death to the\n * namespace init, and the kernel then SIGKILLs every remaining task in that\n * namespace — including non-cooperative workers and detached (`setsid`)\n * descendants. Signal delivery is never treated as termination.\n * - termination witness: `probeNamespace` detects the recorded namespace\n * init's death and `awaitBoundaryDrained` waits for it. When a PID\n * namespace's init exits, the kernel SIGKILLs every remaining task in that\n * namespace, so init death is the exact termination witness — a `close`\n * event, a resolved promise, or a dead direct child with surviving\n * descendants is not. `namespaceMemberPids` remains as a diagnostic\n * enumeration only: host process-table scans are environment-dependent\n * (zombies keep their ns link until reaped; foreign same-uid tasks can be\n * unreadable) and have poisoned drains on kernels where either occurs.\n */\nexport const SUPERVISOR_BINARY = \"/usr/bin/bwrap\";\n\nexport const SUPERVISOR_SYSTEM_ARGS = [\n\t\"--unshare-all\",\n\t\"--die-with-parent\",\n\t\"--new-session\",\n\t\"--cap-drop\",\n\t\"ALL\",\n\t\"--clearenv\",\n\t\"--ro-bind\",\n\t\"/usr\",\n\t\"/usr\",\n\t\"--symlink\",\n\t\"usr/bin\",\n\t\"/bin\",\n\t\"--symlink\",\n\t\"usr/lib\",\n\t\"/lib\",\n\t\"--symlink\",\n\t\"usr/lib64\",\n\t\"/lib64\",\n\t\"--proc\",\n\t\"/proc\",\n\t\"--dev\",\n\t\"/dev\",\n\t\"--tmpfs\",\n\t\"/tmp\",\n\t\"--setenv\",\n\t\"PATH\",\n\t\"/usr/bin:/bin\",\n\t\"--setenv\",\n\t\"LANG\",\n\t\"C.UTF-8\",\n] as const;\n\nexport type SupervisorChild = { readonly kill: (signal: NodeJS.Signals) => boolean };\n\n/**\n * The only termination guarantee this package honors: a private PID namespace\n * plus the Linux bwrap supervisor. Anything else is refused at the boundary.\n */\nexport function loadSupervisorBackend(): { readonly binary: string; readonly args: readonly string[] } {\n\tif (process.platform !== \"linux\" || !existsSync(SUPERVISOR_BINARY))\n\t\tthrow new VerifiedRunError(\"unsupported_boundary\");\n\treturn { binary: SUPERVISOR_BINARY, args: SUPERVISOR_SYSTEM_ARGS };\n}\n\n/**\n * SIGKILL escalation to the supervised direct child. `--die-with-parent` makes\n * the kernel kill the whole PID namespace once this supervisor dies, so no\n * per-descendant signaling (which PID reuse would make unsafe) is needed.\n */\nexport function escalateTermination(child: SupervisorChild): void {\n\tchild.kill(\"SIGKILL\");\n}\n\n/**\n * Enumerate host PIDs of every task still inside `identity.namespace`.\n *\n * Membership is keyed on the recorded namespace symlink, not on the init's\n * PID: a fabricated or reaped direct child cannot mask surviving descendants.\n *\n * A task whose namespace link cannot be read is only a candidate member when\n * it could belong to the recorded boundary at all. The backend's members\n * always run under our real uid — the pid namespace is owned by a user\n * namespace we created and `CAP_SETUID` is dropped — so a task owned by\n * another uid, or one that vanished mid-scan, cannot be a member and is\n * skipped. An unreadable task owned by our uid is conservatively counted:\n * failing to prove emptiness must fail closed.\n */\nexport function namespaceMemberPids(identity: Pick<NamespaceIdentity, \"namespace\">): number[] {\n\tconst members: number[] = [];\n\t// A platform without getuid cannot exclude foreign-owned tasks, so every\n\t// unreadable entry stays a candidate member (fail closed).\n\tconst uid = typeof process.getuid === \"function\" ? process.getuid() : undefined;\n\tfor (const entry of readdirSync(\"/proc\")) {\n\t\tif (!/^\\d+$/.test(entry)) continue;\n\t\ttry {\n\t\t\tif (readlinkSync(`/proc/${entry}/ns/pid`) !== identity.namespace) continue;\n\t\t\t// A zombie still carries the namespace link until reaped, but it\n\t\t\t// cannot run: counting it as a member confuses teardown with\n\t\t\t// survival.\n\t\t\tconst stat = readFileSync(`/proc/${entry}/stat`, \"utf8\");\n\t\t\tconst state = stat\n\t\t\t\t.slice(stat.lastIndexOf(\")\") + 2)\n\t\t\t\t.trim()\n\t\t\t\t.split(/\\s+/)[0];\n\t\t\tif (state !== \"Z\" && state !== \"X\") members.push(Number(entry));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error)) continue;\n\t\t\tif (error.code === \"ENOENT\" || error.code === \"ESRCH\") continue;\n\t\t\ttry {\n\t\t\t\tif (uid === undefined || statSync(`/proc/${entry}`).uid === uid) members.push(Number(entry));\n\t\t\t} catch (statError) {\n\t\t\t\tif (\n\t\t\t\t\t!(statError instanceof Error && \"code\" in statError) ||\n\t\t\t\t\t(statError.code !== \"ENOENT\" && statError.code !== \"ESRCH\")\n\t\t\t\t)\n\t\t\t\t\tmembers.push(Number(entry));\n\t\t\t}\n\t\t}\n\t}\n\treturn members;\n}\n\n/** Init death terminates the whole namespace; a zombie init already proves teardown finished. */\nexport function probeOwnedNamespace(identity: NamespaceIdentity): \"terminated\" | \"alive\" | \"unknown\" {\n\tconst status = probeNamespace(identity);\n\treturn status === \"gone\" ? \"terminated\" : status;\n}\n\n/**\n * Wait for the namespace init's death within budgetMs. Init death is the\n * termination witness: the kernel SIGKILLs every remaining task in the PID\n * namespace when its init exits, so a drained namespace needs no process-table\n * enumeration. `populated`/`unknown` never release claims.\n *\n * `unknown` is not terminal here: teardown has a transient window where the\n * init's stat is readable but its ns link read is denied or the identity\n * probe is inconclusive, and that window resolves to `gone` once the kernel\n * finishes teardown. Only a probe that is still inconclusive at the deadline\n * stays `unknown`; a still-alive init is `populated`.\n */\nexport async function awaitBoundaryDrained(\n\tidentity: NamespaceIdentity,\n\tbudgetMs: number,\n): Promise<\"drained\" | \"populated\" | \"unknown\"> {\n\tconst deadline = performance.now() + Math.max(0, budgetMs);\n\tfor (;;) {\n\t\tconst status = probeNamespace(identity);\n\t\tif (status === \"gone\") return \"drained\";\n\t\tif (performance.now() >= deadline) return status === \"alive\" ? \"populated\" : \"unknown\";\n\t\tawait delay(Math.min(10, Math.max(1, deadline - performance.now())));\n\t}\n}\n"]}
1
+ {"version":3,"file":"supervisor-adapter.js","sourceRoot":"","sources":["../../../src/core/verified-run/supervisor-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { type NamespaceIdentity, probeNamespace } from \"./namespace-identity.ts\";\nimport { VerifiedRunError } from \"./storage.ts\";\n\n/**\n * Owned-process supervisor boundary (WP02).\n *\n * Three concerns are separated here so the broker only orchestrates them:\n *\n * - spawn: `SUPERVISOR_SYSTEM_ARGS` + `loadSupervisorBackend` describe the only\n * backend this package trusts — bubblewrap with `--unshare-all`, which puts\n * the supervised command in a fresh PID namespace whose init is the only\n * process the host can see. A platform without that backend has no way to\n * prove termination, so callers must refuse with `unsupported_boundary`\n * instead of degrading to best-effort signaling.\n * - cancel: `escalateTermination` sends SIGKILL to the supervised direct\n * child (the bwrap supervisor). `--die-with-parent` propagates death to the\n * namespace init, and the kernel then SIGKILLs every remaining task in that\n * namespace — including non-cooperative workers and detached (`setsid`)\n * descendants. Signal delivery is never treated as termination.\n * - termination witness: `probeNamespace` detects the recorded namespace\n * init's death and `awaitBoundaryDrained` waits for it. When a PID\n * namespace's init exits, the kernel SIGKILLs every remaining task in that\n * namespace, so init death is the exact termination witness — a `close`\n * event, a resolved promise, or a dead direct child with surviving\n * descendants is not. `namespaceMemberPids` remains as a diagnostic\n * enumeration only: host process-table scans are environment-dependent\n * (zombies keep their ns link until reaped; foreign same-uid tasks can be\n * unreadable) and have poisoned drains on kernels where either occurs.\n */\nexport const SUPERVISOR_BINARY = \"/usr/bin/bwrap\";\n\nexport const SUPERVISOR_SYSTEM_ARGS = [\n\t\"--unshare-all\",\n\t\"--die-with-parent\",\n\t\"--new-session\",\n\t\"--cap-drop\",\n\t\"ALL\",\n\t\"--clearenv\",\n\t\"--ro-bind\",\n\t\"/usr\",\n\t\"/usr\",\n\t\"--symlink\",\n\t\"usr/bin\",\n\t\"/bin\",\n\t\"--symlink\",\n\t\"usr/lib\",\n\t\"/lib\",\n\t\"--symlink\",\n\t\"usr/lib64\",\n\t\"/lib64\",\n\t\"--proc\",\n\t\"/proc\",\n\t\"--dev\",\n\t\"/dev\",\n\t\"--tmpfs\",\n\t\"/tmp\",\n\t\"--setenv\",\n\t\"PATH\",\n\t\"/usr/bin:/bin\",\n\t\"--setenv\",\n\t\"LANG\",\n\t\"C.UTF-8\",\n] as const;\n\nexport type SupervisorChild = { readonly kill: (signal: NodeJS.Signals) => boolean };\n\n/**\n * The only termination guarantee this package honors: a private PID namespace\n * plus the Linux bwrap supervisor. Anything else is refused at the boundary.\n */\nexport function loadSupervisorBackend(): { readonly binary: string; readonly args: readonly string[] } {\n\tif (process.platform !== \"linux\" || !existsSync(SUPERVISOR_BINARY))\n\t\tthrow new VerifiedRunError(\"unsupported_boundary\");\n\treturn { binary: SUPERVISOR_BINARY, args: SUPERVISOR_SYSTEM_ARGS };\n}\n\n/**\n * SIGKILL escalation to the supervised direct child. `--die-with-parent` makes\n * the kernel kill the whole PID namespace once this supervisor dies, so no\n * per-descendant signaling (which PID reuse would make unsafe) is needed.\n */\nexport function escalateTermination(child: SupervisorChild): void {\n\tchild.kill(\"SIGKILL\");\n}\n\n/**\n * Enumerate host PIDs of every task still inside `identity.namespace`.\n *\n * Membership is keyed on the recorded namespace symlink, not on the init's\n * PID: a fabricated or reaped direct child cannot mask surviving descendants.\n *\n * A task whose namespace link cannot be read is only a candidate member when\n * it could belong to the recorded boundary at all. The backend's members\n * always run under our real uid — the pid namespace is owned by a user\n * namespace we created and `CAP_SETUID` is dropped — so a task owned by\n * another uid, or one that vanished mid-scan, cannot be a member and is\n * skipped. An unreadable task owned by our uid is conservatively counted:\n * failing to prove emptiness must fail closed.\n */\nexport function namespaceMemberPids(identity: Pick<NamespaceIdentity, \"namespace\">): number[] {\n\tconst members: number[] = [];\n\t// A platform without getuid cannot exclude foreign-owned tasks, so every\n\t// unreadable entry stays a candidate member (fail closed).\n\tconst uid = typeof process.getuid === \"function\" ? process.getuid() : undefined;\n\tfor (const entry of readdirSync(\"/proc\")) {\n\t\tif (!/^\\d+$/.test(entry)) continue;\n\t\ttry {\n\t\t\tif (readlinkSync(`/proc/${entry}/ns/pid`) !== identity.namespace) continue;\n\t\t\t// A zombie still carries the namespace link until reaped, but it\n\t\t\t// cannot run: counting it as a member confuses teardown with\n\t\t\t// survival.\n\t\t\tconst stat = readFileSync(`/proc/${entry}/stat`, \"utf8\");\n\t\t\tconst state = stat\n\t\t\t\t.slice(stat.lastIndexOf(\")\") + 2)\n\t\t\t\t.trim()\n\t\t\t\t.split(/\\s+/)[0];\n\t\t\tif (state !== \"Z\" && state !== \"X\") members.push(Number(entry));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error)) continue;\n\t\t\tif (error.code === \"ENOENT\" || error.code === \"ESRCH\") continue;\n\t\t\ttry {\n\t\t\t\tif (uid === undefined || statSync(`/proc/${entry}`).uid === uid) members.push(Number(entry));\n\t\t\t} catch (statError) {\n\t\t\t\tif (\n\t\t\t\t\t!(statError instanceof Error && \"code\" in statError) ||\n\t\t\t\t\t(statError.code !== \"ENOENT\" && statError.code !== \"ESRCH\")\n\t\t\t\t)\n\t\t\t\t\tmembers.push(Number(entry));\n\t\t\t}\n\t\t}\n\t}\n\treturn members;\n}\n\n/** Init death terminates the whole namespace; a zombie init already proves teardown finished. */\nexport function probeOwnedNamespace(identity: NamespaceIdentity): \"terminated\" | \"alive\" | \"unknown\" {\n\tconst status = probeNamespace(identity);\n\treturn status === \"gone\" ? \"terminated\" : status;\n}\n\n/**\n * Wait for the namespace init's death within budgetMs. Init death is the\n * termination witness: the kernel SIGKILLs every remaining task in the PID\n * namespace when its init exits, so a drained namespace needs no process-table\n * enumeration. `populated`/`unknown` never release claims.\n *\n * `unknown` is not terminal here: teardown has a transient window where the\n * init's stat is readable but its ns link read is denied or the identity\n * probe is inconclusive, and that window resolves to `gone` once the kernel\n * finishes teardown. Only a probe that is still inconclusive at the deadline\n * stays `unknown`; a still-alive init is `populated`.\n *\n * `drained` means the kernel has provably killed every member; a task frozen\n * in uninterruptible kernel sleep can linger in /proc briefly afterwards —\n * that is kernel bookkeeping, not a live boundary.\n */\nexport async function awaitBoundaryDrained(\n\tidentity: NamespaceIdentity,\n\tbudgetMs: number,\n): Promise<\"drained\" | \"populated\" | \"unknown\"> {\n\tconst deadline = performance.now() + Math.max(0, budgetMs);\n\tfor (;;) {\n\t\tconst status = probeNamespace(identity);\n\t\tif (status === \"gone\") return \"drained\";\n\t\tif (performance.now() >= deadline) return status === \"alive\" ? \"populated\" : \"unknown\";\n\t\tawait delay(Math.min(10, Math.max(1, deadline - performance.now())));\n\t}\n}\n"]}
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-custom-provider-anthropic",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-custom-provider-anthropic",
9
- "version": "1.2.2",
9
+ "version": "1.2.3",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "1.2.2",
4
+ "version": "1.2.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "1.2.2",
4
+ "version": "1.2.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-gondolin",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-gondolin",
9
- "version": "1.2.2",
9
+ "version": "1.2.3",
10
10
  "dependencies": {
11
11
  "@earendil-works/gondolin": "0.12.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-gondolin",
3
3
  "private": true,
4
- "version": "1.2.2",
4
+ "version": "1.2.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-sandbox",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-sandbox",
9
- "version": "1.2.2",
9
+ "version": "1.2.3",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sandbox-runtime": "0.0.26"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-sandbox",
3
3
  "private": true,
4
- "version": "1.2.2",
4
+ "version": "1.2.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omk-extension-with-deps",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omk-extension-with-deps",
9
- "version": "1.2.2",
9
+ "version": "1.2.3",
10
10
  "dependencies": {
11
11
  "ms": "2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omk-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.2.2",
4
+ "version": "1.2.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "open-multi-agent-kit",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "open-multi-agent-kit",
9
- "version": "1.2.2",
9
+ "version": "1.2.3",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "omk-agent-core": "^1.2.2",
13
- "omk-ai": "^1.2.2",
14
- "omk-tui": "^1.2.2",
12
+ "omk-agent-core": "^1.2.3",
13
+ "omk-ai": "^1.2.3",
14
+ "omk-tui": "^1.2.3",
15
15
  "@silvia-odwyer/photon-node": "0.3.4",
16
16
  "canonicalize": "4.0.0",
17
17
  "chalk": "5.6.2",
@@ -27,8 +27,8 @@
27
27
  "typebox": "1.3.11",
28
28
  "undici": "8.10.0",
29
29
  "yaml": "2.9.0",
30
- "omk-adaptorch-wpl": "^1.2.2",
31
- "omk-protocol": "^1.2.2"
30
+ "omk-adaptorch-wpl": "^1.2.3",
31
+ "omk-protocol": "^1.2.3"
32
32
  },
33
33
  "optionalDependencies": {
34
34
  "@mariozechner/clipboard": "0.3.9"
@@ -1285,19 +1285,19 @@
1285
1285
  }
1286
1286
  },
1287
1287
  "node_modules/omk-adaptorch-wpl": {
1288
- "version": "1.2.2",
1289
- "resolved": "https://registry.npmjs.org/omk-adaptorch-wpl/-/omk-adaptorch-wpl-1.2.2.tgz",
1288
+ "version": "1.2.3",
1289
+ "resolved": "https://registry.npmjs.org/omk-adaptorch-wpl/-/omk-adaptorch-wpl-1.2.3.tgz",
1290
1290
  "license": "MIT",
1291
1291
  "engines": {
1292
1292
  "node": ">=22.19.0"
1293
1293
  }
1294
1294
  },
1295
1295
  "node_modules/omk-agent-core": {
1296
- "version": "1.2.2",
1297
- "resolved": "https://registry.npmjs.org/omk-agent-core/-/omk-agent-core-1.2.2.tgz",
1296
+ "version": "1.2.3",
1297
+ "resolved": "https://registry.npmjs.org/omk-agent-core/-/omk-agent-core-1.2.3.tgz",
1298
1298
  "license": "MIT",
1299
1299
  "dependencies": {
1300
- "omk-ai": "^1.2.2",
1300
+ "omk-ai": "^1.2.3",
1301
1301
  "ignore": "7.0.6",
1302
1302
  "typebox": "1.3.11",
1303
1303
  "yaml": "2.9.0"
@@ -1307,8 +1307,8 @@
1307
1307
  }
1308
1308
  },
1309
1309
  "node_modules/omk-ai": {
1310
- "version": "1.2.2",
1311
- "resolved": "https://registry.npmjs.org/omk-ai/-/omk-ai-1.2.2.tgz",
1310
+ "version": "1.2.3",
1311
+ "resolved": "https://registry.npmjs.org/omk-ai/-/omk-ai-1.2.3.tgz",
1312
1312
  "license": "MIT",
1313
1313
  "dependencies": {
1314
1314
  "@anthropic-ai/sdk": "0.91.1",
@@ -1330,16 +1330,16 @@
1330
1330
  }
1331
1331
  },
1332
1332
  "node_modules/omk-protocol": {
1333
- "version": "1.2.2",
1334
- "resolved": "https://registry.npmjs.org/omk-protocol/-/omk-protocol-1.2.2.tgz",
1333
+ "version": "1.2.3",
1334
+ "resolved": "https://registry.npmjs.org/omk-protocol/-/omk-protocol-1.2.3.tgz",
1335
1335
  "license": "MIT",
1336
1336
  "engines": {
1337
1337
  "node": ">=22.19.0"
1338
1338
  }
1339
1339
  },
1340
1340
  "node_modules/omk-tui": {
1341
- "version": "1.2.2",
1342
- "resolved": "https://registry.npmjs.org/omk-tui/-/omk-tui-1.2.2.tgz",
1341
+ "version": "1.2.3",
1342
+ "resolved": "https://registry.npmjs.org/omk-tui/-/omk-tui-1.2.3.tgz",
1343
1343
  "license": "MIT",
1344
1344
  "dependencies": {
1345
1345
  "get-east-asian-width": "1.6.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-multi-agent-kit",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "OMK (Open Multi-Agent Kit) coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "omkConfig": {
@@ -43,9 +43,9 @@
43
43
  "link-workspace-deps": "node ./scripts/link-workspace-deps.mjs"
44
44
  },
45
45
  "dependencies": {
46
- "omk-agent-core": "^1.2.2",
47
- "omk-ai": "^1.2.2",
48
- "omk-tui": "^1.2.2",
46
+ "omk-agent-core": "^1.2.3",
47
+ "omk-ai": "^1.2.3",
48
+ "omk-tui": "^1.2.3",
49
49
  "@silvia-odwyer/photon-node": "0.3.4",
50
50
  "canonicalize": "4.0.0",
51
51
  "chalk": "5.6.2",
@@ -61,8 +61,8 @@
61
61
  "typebox": "1.3.11",
62
62
  "undici": "8.10.0",
63
63
  "yaml": "2.9.0",
64
- "omk-adaptorch-wpl": "^1.2.2",
65
- "omk-protocol": "^1.2.2"
64
+ "omk-adaptorch-wpl": "^1.2.3",
65
+ "omk-protocol": "^1.2.3"
66
66
  },
67
67
  "overrides": {
68
68
  "rimraf": "6.1.2",