From 7b162b823109d0055e19a241d430c3aeb8eaf56a Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:06:54 +0800 Subject: [PATCH 001/166] docs: define M-040 runtime and integration ownership --- .../architecture/runtime-control-ownership.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/architecture/runtime-control-ownership.md diff --git a/docs/architecture/runtime-control-ownership.md b/docs/architecture/runtime-control-ownership.md new file mode 100644 index 00000000..79935a89 --- /dev/null +++ b/docs/architecture/runtime-control-ownership.md @@ -0,0 +1,227 @@ +# Runtime, Control and Integration Ownership + +> **Status**: M-040 MS-001 architecture baseline +> **Date**: 2026-09-29 +> **Purpose**: define authoritative ownership and dependency direction before Cortex introduces the canonical Protocol/SDK, RuntimePort, Control Service, Project Integration and Extension contracts. + +## 1. Position + +Cortex remains a governance and orchestration framework. M-040 does not replace existing Management API, Coordination, Dispatch, Workspace, Topology, Agent Adapter or Event Bus implementations with a new runtime kernel. + +Instead, later milestones introduce stable contracts **over** existing authoritative owners. + +## 2. Layer model + +```mermaid +flowchart TB + S["CLI / Dashboard / MCP / Plugins"] + SDK["Future Cortex SDK / Capability Facade"] + P["Canonical Protocol / Refs"] + + subgraph OWNERS["Authoritative Owners"] + M["Management API"] + C["Coordination"] + T["Topology"] + W["Workspace Runtime"] + A["Agent / Adapter Layer"] + E["Event Bus + Domain Journals"] + end + + CTRL["Future Control Service"] + RP["Future RuntimePort"] + + S --> SDK --> P + P --> M + P --> C + P --> T + P --> W + P --> A + P --> E + + M --> CTRL + C --> CTRL + T --> CTRL + W --> CTRL + CTRL --> RP + A --> RP +``` + +The future SDK and protocol own no new persistence. + +## 3. Authoritative ownership matrix + +| Concern | Authoritative owner | Notes | +| :--- | :--- | :--- | +| Mission / Milestone / workflow truth | existing `.agent` mission/workflow state | governance truth | +| Coordination task lifecycle | `lib/coordination/contract.js`, `state.js`, journal/service | deterministic fail-closed state machine | +| Management projections | Management API | query/projection boundary | +| Run / Queue / Session records | existing Management/Collaboration contracts | observable/governance runtime records | +| Decisions / Waitpoints / Inbox | Management API governed writers + state | authorization/coordination | +| Agent registry | `lib/agents/registry.js` | agent identity/capabilities | +| External agent adapter contract | `lib/agents/adapters/*` | preserve behind future RuntimePort bridge | +| Governed manual dispatch orchestration | `lib/dispatch/*` | plan/gate/lease/idempotency composition | +| Transport execution | `lib/agents/dispatch-execute.js` and concrete adapters | below orchestration boundary | +| Runtime capability descriptor/matching | `lib/runtime-adapters/*` | existing capability/routing base | +| Host control | `lib/host-adapter/*` | gated host-specific control | +| Project topology | `lib/topology/index.js` + `.agent/topology/projects.json` | project identity / peers / capabilities | +| Workspace lifecycle | `.agent/workspaces/scripts/workspace-runtime.js` + schemas | identities/hooks/leases/composites | +| Framework event transport | `lib/event-bus/*` | transport/persistence/subscription | +| Coordination event truth | coordination journal | domain-authoritative journal | +| Dashboard process lifecycle | `lib/dashboard/supervisor.js` | operational process state only | +| MCP | `lib/mcp/*` | surface/adapter, not domain owner | +| CLI | `bin/cli.js` + `lib/commands/*` | routing/surface, not canonical owner | + +## 4. Important non-collapsing boundaries + +### Coordination state != Runtime state + +Coordination already owns the software-work task lifecycle. RuntimePort will represent backend execution lifecycle only. + +A runtime reporting `completed` MUST NOT directly transition a Mission or Coordination Task to completed. + +### Framework Event Bus != universal domain state store + +The Event Bus provides durable event transport, dedupe, ack, subscription and history. Domain journals may remain authoritative. + +Future `CortexEvent` is a canonical envelope/projection contract, not a requirement to rebuild all state solely from one global event log. + +### Project topology != Runtime topology + +Project topology identifies repositories/projects and cross-project relations. + +Runtime topology identifies hosts/runtime endpoints/execution capabilities. + +They may reference each other but must remain distinct concepts. + +### Workspace identity != Project identity + +A project may have multiple workspaces/worktrees. Workspace state must continue to be owned by the existing workspace runtime. + +### Dispatch orchestration != Transport executor + +```text +Governed Dispatch / Control + | + v + RuntimePort + | + v +Adapter / Transport Executor +``` + +`lib/dispatch/execute.js` and `lib/agents/dispatch-execute.js` currently represent different layers and must not be merged casually. + +## 5. Existing capability architecture + +`lib/runtime-adapters/capability-contract.js` already defines a versioned Host Capability Descriptor with closed capability names, levels and evidence sources. + +M-040 will extend this architecture into separate capability namespaces: + +```text +Host observability capabilities +Runtime lifecycle capabilities +Project capabilities +Extension permissions +``` + +These share versioning/naming conventions but are not one flat enum. + +Existing capability matching and dispatch policy are migration inputs for RuntimePort/Topology work. + +## 6. External project integration + +External Project Integration MUST evolve the current topology owner instead of adding a second project registry. + +```text +Project topology + | + +-- self / peer identity + +-- roles + +-- capabilities + +-- topology references + +-- bridge subscriptions + | + +-- future Project Integration descriptor +``` + +Projects such as Axrail remain authoritative for their own domain/runtime semantics. + +Cortex may govern Missions, validation, evidence, decisions and cross-repository coordination without taking ownership of Axrail's HarnessRuntime, ToolRuntime, Policy, Approval, Validation or TransactionRuntime. + +## 7. Workspace and cross-project integration + +The existing workspace runtime already owns: + +- WorkspaceIdentity +- HookLifecycle +- ResourceLease +- CompositeWorkspace +- ordered cross-repository recovery + +Future SDK/project contracts wrap these capabilities rather than creating another workspace persistence layer. + +Cross-repository merge remains non-atomic. + +## 8. Current surface coupling + +Today `bin/cli.js` routes directly to many implementation modules. That is acceptable for command routing, but shared canonical capabilities should gradually move behind the M-040 SDK facade. + +The SDK is not intended to wrap every utility command. Priority is: + +1. project resolution / Management query; +2. canonical refs and capability discovery; +3. topology/project lookup; +4. Run / Queue / Session queries; +5. Decision / Waitpoint queries; +6. Agent discovery; +7. Workspace reads; +8. RuntimePort. + +## 9. Architecture guards + +M-040 will extend the existing architecture-guard/testing approach. + +Required future guards include: + +- canonical protocol/domain code cannot import Paseo, Axrail or provider-specific modules; +- SDK facade cannot own a second persistent state store; +- RuntimePort cannot directly complete Missions/Coordination Tasks; +- Control Service cannot bypass workflow/Decision/Waitpoint authorization; +- Project Integration cannot create a second ProjectRegistry beside topology; +- Workspace facade cannot create a second workspace state store; +- MCP/Dashboard/UI cannot mutate governed state through private file writes; +- runtime events cannot become governance commands without owner validation; +- native execution must remain functional without Paseo. + +The existing `tests/management/management-interface-boundary.test.js` is a useful behavioral guard seed. + +The existing `architecture-guard` skill currently contains sample-level checks and must be upgraded before it can enforce these boundaries. + +## 10. MS-002 migration baseline + +MS-002 should introduce the smallest useful canonical facade first. + +Proposed initial namespaces: + +```text +project.* +management.query.* +topology.* +capabilities.* +refs.* +``` + +No broad rewrite is required. + +Existing implementation modules remain behind adapters until later milestones migrate them deliberately. + +## 11. Architecture invariants + +1. One authoritative owner per state type. +2. Facades/protocols do not create duplicate state. +3. Runtime details never leak into core governance domain. +4. Project integration preserves target-project autonomy. +5. Runtime completion is evidence, not governance completion. +6. Recommendations/routing scores are never authorization. +7. Optional Control Service adds coordination, not new truth. +8. Existing native adapter/direct mode remains supported. From 0965d11fccc7beeb5b9bd01573a0ec33520b2a8e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:06:57 +0800 Subject: [PATCH 002/166] docs: index M-040 ownership architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 37ff2baa..05fca4d2 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -26,6 +26,7 @@ - [AI-Native SDLC 闭环治理](./ai-native-sdlc-governance.md) - [Agent Workspace Orchestration](./agent-workspace-orchestration.md) - [Agent Runtime Continuity](./agent-runtime-continuity.md) +- [Runtime, Control and Integration Ownership](./runtime-control-ownership.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 8856326dbe9f50586888db328c3664dd05d17406 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:11:58 +0800 Subject: [PATCH 003/166] feat(m040): add pnpm-workspace.yaml --- pnpm-workspace.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pnpm-workspace.yaml diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..dee51e92 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" From 538a353290bd8be8bfee0bf87f67b227e04a8e36 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:00 +0800 Subject: [PATCH 004/166] feat(m040): add packages/protocol/package.json --- packages/protocol/package.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/protocol/package.json diff --git a/packages/protocol/package.json b/packages/protocol/package.json new file mode 100644 index 00000000..7240e00a --- /dev/null +++ b/packages/protocol/package.json @@ -0,0 +1,14 @@ +{ + "name": "@cortex-agent/protocol", + "version": "0.0.0", + "private": true, + "description": "Canonical portable contracts for Cortex Agent", + "main": "src/index.js", + "exports": { + ".": "./src/index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "license": "MIT" +} From bff14ae79adeb5b8bdb25f40d77a728f47a5f956 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:03 +0800 Subject: [PATCH 005/166] feat(m040): add packages/protocol/src/refs.js --- packages/protocol/src/refs.js | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/protocol/src/refs.js diff --git a/packages/protocol/src/refs.js b/packages/protocol/src/refs.js new file mode 100644 index 00000000..cb296a41 --- /dev/null +++ b/packages/protocol/src/refs.js @@ -0,0 +1,48 @@ +"use strict"; + +const REF_KINDS = Object.freeze([ + "project", + "workspace", + "run", + "agent", + "host", + "runtime", + "session", +]); + +const REF_KIND_SET = new Set(REF_KINDS); +const REF_PATTERN = /^([a-z][a-z0-9-]*):(.+)$/; + +function createRef(kind, value) { + if (!REF_KIND_SET.has(kind)) { + const error = new Error(`Unknown Cortex ref kind: ${kind}`); + error.code = "ERR_REF_KIND_UNKNOWN"; + throw error; + } + const id = String(value || "").trim(); + if (!id || /[\r\n]/.test(id)) { + const error = new Error("Cortex ref value must be a non-empty single-line string."); + error.code = "ERR_REF_VALUE_INVALID"; + throw error; + } + return `${kind}:${id}`; +} + +function parseRef(value, expectedKind) { + if (typeof value !== "string") return null; + const match = REF_PATTERN.exec(value); + if (!match || !REF_KIND_SET.has(match[1])) return null; + if (expectedKind && match[1] !== expectedKind) return null; + return Object.freeze({ kind: match[1], value: match[2], ref: value }); +} + +function isRef(value, expectedKind) { + return parseRef(value, expectedKind) !== null; +} + +module.exports = { + REF_KINDS, + createRef, + parseRef, + isRef, +}; From 485b4b238039d23a3ed8f371a1b47121f23beb79 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:06 +0800 Subject: [PATCH 006/166] feat(m040): add packages/protocol/src/version.js --- packages/protocol/src/version.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/protocol/src/version.js diff --git a/packages/protocol/src/version.js b/packages/protocol/src/version.js new file mode 100644 index 00000000..2eea21ea --- /dev/null +++ b/packages/protocol/src/version.js @@ -0,0 +1,17 @@ +"use strict"; + +const PROTOCOL_NAME = "cortex"; +const PROTOCOL_VERSION = "1.0"; +const RUNTIME_PROTOCOL_NAME = "cortex-runtime"; +const RUNTIME_PROTOCOL_VERSION = "1.0"; +const PROJECT_PROTOCOL_NAME = "cortex-project"; +const PROJECT_PROTOCOL_VERSION = "1.0"; + +module.exports = { + PROTOCOL_NAME, + PROTOCOL_VERSION, + RUNTIME_PROTOCOL_NAME, + RUNTIME_PROTOCOL_VERSION, + PROJECT_PROTOCOL_NAME, + PROJECT_PROTOCOL_VERSION, +}; From 79a288b478f2642baae08eae14ca07e6d1c1ef63 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:08 +0800 Subject: [PATCH 007/166] feat(m040): add packages/protocol/src/result.js --- packages/protocol/src/result.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/protocol/src/result.js diff --git a/packages/protocol/src/result.js b/packages/protocol/src/result.js new file mode 100644 index 00000000..9a770de1 --- /dev/null +++ b/packages/protocol/src/result.js @@ -0,0 +1,15 @@ +"use strict"; + +function ok(data, meta = {}) { + return Object.freeze({ ok: true, data, ...meta }); +} + +function fail(code, message, details = {}, meta = {}) { + return Object.freeze({ + ok: false, + error: Object.freeze({ code, message, details }), + ...meta, + }); +} + +module.exports = { ok, fail }; From e2e84d6d3ca3d12c7c254dc4aed98c17fa4b42a5 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:11 +0800 Subject: [PATCH 008/166] feat(m040): add packages/protocol/src/index.js --- packages/protocol/src/index.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/protocol/src/index.js diff --git a/packages/protocol/src/index.js b/packages/protocol/src/index.js new file mode 100644 index 00000000..481de126 --- /dev/null +++ b/packages/protocol/src/index.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + ...require("./refs"), + ...require("./version"), + ...require("./result"), +}; From bb1f087f6a59e45646fde26ed92867cf78f1ee7e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:14 +0800 Subject: [PATCH 009/166] feat(m040): add packages/sdk/package.json --- packages/sdk/package.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/sdk/package.json diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 00000000..0b3f726f --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,17 @@ +{ + "name": "@cortex-agent/sdk", + "version": "0.0.0", + "private": true, + "description": "Stable consumer facade for Cortex Agent capabilities", + "main": "src/index.js", + "exports": { + ".": "./src/index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "license": "MIT", + "dependencies": { + "@cortex-agent/protocol": "workspace:*" + } +} From 0d4aa38daf0dab68645a6c738fabf02a8a91142b Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:17 +0800 Subject: [PATCH 010/166] feat(m040): add packages/sdk/src/index.js --- packages/sdk/src/index.js | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/sdk/src/index.js diff --git a/packages/sdk/src/index.js b/packages/sdk/src/index.js new file mode 100644 index 00000000..9b44dcbb --- /dev/null +++ b/packages/sdk/src/index.js @@ -0,0 +1,62 @@ +"use strict"; + +const protocol = require("@cortex-agent/protocol"); + +function requireTransport(transport) { + if (!transport || typeof transport.query !== "function") { + const error = new Error("Cortex SDK requires a transport with query(projection, filters)."); + error.code = "ERR_SDK_TRANSPORT_REQUIRED"; + throw error; + } + return transport; +} + +function createCortexClient(options = {}) { + const transport = requireTransport(options.transport); + + async function query(projection, filters = {}) { + return transport.query(projection, filters); + } + + return Object.freeze({ + protocol, + project: Object.freeze({ + resolve: typeof transport.resolveProject === "function" + ? () => transport.resolveProject() + : undefined, + }), + capabilities: Object.freeze({ + discover: typeof transport.discoverCapabilities === "function" + ? () => transport.discoverCapabilities() + : async () => ({ protocol_version: protocol.PROTOCOL_VERSION, capabilities: [] }), + }), + tasks: Object.freeze({ + get: (taskId) => query("task-state", { task: taskId }), + }), + runs: Object.freeze({ + list: () => query("runs"), + get: (runId) => query("run-state", { run: runId }), + }), + decisions: Object.freeze({ + list: () => query("decisions"), + }), + waitpoints: Object.freeze({ + list: () => query("waitpoints"), + }), + coordination: Object.freeze({ + tasks: Object.freeze({ + list: (filters = {}) => query("coordination-tasks", filters), + get: (taskId) => query("coordination-tasks", { task: taskId }), + }), + }), + topology: Object.freeze({ + get: typeof transport.getTopology === "function" + ? () => transport.getTopology() + : undefined, + }), + }); +} + +module.exports = { + createCortexClient, +}; From 396082099ca81a600a6104e013972e380034de1a Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:20 +0800 Subject: [PATCH 011/166] feat(m040): add tests/architecture/m040-monorepo-boundary.test.js --- .../m040-monorepo-boundary.test.js | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/architecture/m040-monorepo-boundary.test.js diff --git a/tests/architecture/m040-monorepo-boundary.test.js b/tests/architecture/m040-monorepo-boundary.test.js new file mode 100644 index 00000000..23641786 --- /dev/null +++ b/tests/architecture/m040-monorepo-boundary.test.js @@ -0,0 +1,47 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); + +test("M-040 workspace declares packages/* only", () => { + const workspace = fs.readFileSync(path.join(ROOT, "pnpm-workspace.yaml"), "utf8"); + assert.match(workspace, /packages:\s*\n\s*-\s*["']packages\/\*["']/); +}); + +test("@cortex-agent/protocol remains portable and implementation-free", () => { + const dir = path.join(ROOT, "packages", "protocol", "src"); + for (const name of fs.readdirSync(dir).filter((item) => item.endsWith(".js"))) { + const text = fs.readFileSync(path.join(dir, name), "utf8"); + for (const forbidden of [ + "node:fs", + "node:child_process", + "../../lib", + "paseo", + "axrail", + ]) { + assert.equal(text.includes(forbidden), false, `${name} must not depend on ${forbidden}`); + } + } +}); + +test("@cortex-agent/sdk depends on protocol but owns no persistence implementation", () => { + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "packages", "sdk", "package.json"), "utf8")); + assert.equal(pkg.dependencies["@cortex-agent/protocol"], "workspace:*"); + const text = fs.readFileSync(path.join(ROOT, "packages", "sdk", "src", "index.js"), "utf8"); + assert.equal(text.includes("node:fs"), false); + assert.equal(text.includes(".agent/"), false); + assert.equal(text.includes("../../lib"), false); +}); + +test("canonical refs are typed opaque strings", () => { + const refs = require(path.join(ROOT, "packages", "protocol", "src", "refs.js")); + const ref = refs.createRef("project", "axrail"); + assert.equal(ref, "project:axrail"); + assert.deepEqual(refs.parseRef(ref), { kind: "project", value: "axrail", ref }); + assert.equal(refs.isRef("runtime:paseo-local", "runtime"), true); + assert.equal(refs.isRef("paseo:raw", "runtime"), false); +}); From 0520217020a104093cb2d388aa5b66043704bbeb Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:22 +0800 Subject: [PATCH 012/166] feat(m040): add tests/sdk/cortex-client.test.js --- tests/sdk/cortex-client.test.js | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/sdk/cortex-client.test.js diff --git a/tests/sdk/cortex-client.test.js b/tests/sdk/cortex-client.test.js new file mode 100644 index 00000000..8e7cd6e5 --- /dev/null +++ b/tests/sdk/cortex-client.test.js @@ -0,0 +1,45 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { createCortexClient } = require(path.join(ROOT, "packages", "sdk", "src", "index.js")); + +test("SDK maps canonical methods to existing Management projections", async () => { + const calls = []; + const client = createCortexClient({ + transport: { + async query(projection, filters = {}) { + calls.push({ projection, filters }); + return { projection, filters }; + }, + async getTopology() { + return { self: { project_id: "cortex-agent" }, peers: [] }; + }, + }, + }); + + await client.tasks.get("T-1"); + await client.runs.list(); + await client.runs.get("R-1"); + await client.decisions.list(); + await client.waitpoints.list(); + await client.coordination.tasks.get("T-2"); + const topology = await client.topology.get(); + + assert.deepEqual(calls, [ + { projection: "task-state", filters: { task: "T-1" } }, + { projection: "runs", filters: {} }, + { projection: "run-state", filters: { run: "R-1" } }, + { projection: "decisions", filters: {} }, + { projection: "waitpoints", filters: {} }, + { projection: "coordination-tasks", filters: { task: "T-2" } }, + ]); + assert.equal(topology.self.project_id, "cortex-agent"); +}); + +test("SDK fails closed without a query transport", () => { + assert.throws(() => createCortexClient(), (error) => error.code === "ERR_SDK_TRANSPORT_REQUIRED"); +}); From 5cf8e1c903db6a9bd640b2d9c94be851539eb08d Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:13:05 +0800 Subject: [PATCH 013/166] chore(m040): register workspace importers --- pnpm-lock.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b60ae17..9780dcb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,3 +7,11 @@ settings: importers: .: {} + + packages/protocol: {} + + packages/sdk: + dependencies: + '@cortex-agent/protocol': + specifier: workspace:* + version: link:../protocol From 2ba5623b698172343bde6834802be37e5de8b999 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:13:07 +0800 Subject: [PATCH 014/166] test(m040): add protocol and sdk boundary test script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f492e8f..4aa782a4 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "release:minor": "npm version minor && npm publish --registry https://registry.npmjs.org/", "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", - "heartbeat": "node .agent/scripts/heartbeat.js" + "heartbeat": "node .agent/scripts/heartbeat.js", + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/sdk/cortex-client.test.js" }, "keywords": [ "ai", From 490e90f27f02961af196d3825aa9357d25268203 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:13:49 +0800 Subject: [PATCH 015/166] feat(m040): add lib/sdk/local-transport.js --- lib/sdk/local-transport.js | 92 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 lib/sdk/local-transport.js diff --git a/lib/sdk/local-transport.js b/lib/sdk/local-transport.js new file mode 100644 index 00000000..17fffaa9 --- /dev/null +++ b/lib/sdk/local-transport.js @@ -0,0 +1,92 @@ +"use strict"; + +const path = require("node:path"); +const { + resolveManagementProject, + queryManagementProject, +} = require("../management/client.js"); +const topology = require("../topology"); + +function filtersToArgs(filters = {}) { + const args = []; + for (const [key, value] of Object.entries(filters)) { + if (value === undefined || value === null || value === "") continue; + const flag = `--${key}`; + if (Array.isArray(value)) { + for (const item of value) args.push(flag, String(item)); + } else if (typeof value === "boolean") { + if (value) args.push(flag); + } else { + args.push(flag, String(value)); + } + } + return args; +} + +function createLocalTransport(ctx) { + if (!ctx || typeof ctx !== "object") { + const error = new Error("Local Cortex transport requires a command context."); + error.code = "ERR_LOCAL_TRANSPORT_CONTEXT_REQUIRED"; + throw error; + } + + function resolve() { + const result = resolveManagementProject(ctx); + if (!result.ok) { + const error = new Error(result.error.message); + error.code = result.error.code; + error.details = result.error.details; + throw error; + } + return result.project; + } + + return Object.freeze({ + async resolveProject() { + const project = resolve(); + return { + project_ref: `project:${path.basename(project.root)}`, + root: project.root, + agent_root: project.agent_root, + }; + }, + + async query(projection, filters = {}) { + const result = queryManagementProject(ctx, projection, filtersToArgs(filters)); + if (!result.ok) { + const error = new Error(result.error.message); + error.code = result.error.code; + error.details = result.error.details; + throw error; + } + return result.payload; + }, + + async getTopology() { + const project = resolve(); + return topology.readTopology(project.root); + }, + + async discoverCapabilities() { + return { + protocol: "cortex", + protocol_version: "1.0", + transport: "local", + capabilities: [ + "management.query", + "tasks.read", + "runs.read", + "decisions.read", + "waitpoints.read", + "coordination.tasks.read", + "topology.read", + ], + }; + }, + }); +} + +module.exports = { + createLocalTransport, + filtersToArgs, +}; From ee6f42cbc43c973e84ad61afa05c24cb4d877765 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:13:52 +0800 Subject: [PATCH 016/166] feat(m040): add lib/sdk/local-client.js --- lib/sdk/local-client.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/sdk/local-client.js diff --git a/lib/sdk/local-client.js b/lib/sdk/local-client.js new file mode 100644 index 00000000..16518f45 --- /dev/null +++ b/lib/sdk/local-client.js @@ -0,0 +1,14 @@ +"use strict"; + +const { createCortexClient } = require("../../packages/sdk/src/index.js"); +const { createLocalTransport } = require("./local-transport.js"); + +function createLocalCortexClient(ctx) { + return createCortexClient({ + transport: createLocalTransport(ctx), + }); +} + +module.exports = { + createLocalCortexClient, +}; From f6ec698a2058e8fa8bee7a7bd0449336cdea20f6 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:13:55 +0800 Subject: [PATCH 017/166] feat(m040): add tests/sdk/local-transport.test.js --- tests/sdk/local-transport.test.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/sdk/local-transport.test.js diff --git a/tests/sdk/local-transport.test.js b/tests/sdk/local-transport.test.js new file mode 100644 index 00000000..80ef3597 --- /dev/null +++ b/tests/sdk/local-transport.test.js @@ -0,0 +1,28 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { filtersToArgs } = require(path.join(ROOT, "lib", "sdk", "local-transport.js")); + +test("local SDK transport converts structured filters to existing Management CLI args", () => { + assert.deepEqual(filtersToArgs({ + task: "T-1", + state: "EXECUTING", + ignored: null, + enabled: true, + }), [ + "--task", "T-1", + "--state", "EXECUTING", + "--enabled", + ]); +}); + +test("local SDK transport repeats array-valued filters deterministically", () => { + assert.deepEqual(filtersToArgs({ host: ["codex", "claude-code"] }), [ + "--host", "codex", + "--host", "claude-code", + ]); +}); From a3cb9015cff6f7230d6a9f5d7b8130625d9c40ef Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:14:05 +0800 Subject: [PATCH 018/166] test(m040): include local SDK transport tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4aa782a4..045634a7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/sdk/cortex-client.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js" }, "keywords": [ "ai", From d997dbe48fe487acb1662b24e6fb6463261c1078 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:01:17 +0800 Subject: [PATCH 019/166] feat(m040): support sync local SDK transports --- packages/sdk/src/index.js | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/sdk/src/index.js b/packages/sdk/src/index.js index 9b44dcbb..1dc0d85b 100644 --- a/packages/sdk/src/index.js +++ b/packages/sdk/src/index.js @@ -1,6 +1,14 @@ "use strict"; -const protocol = require("@cortex-agent/protocol"); +let protocol; +try { + protocol = require("@cortex-agent/protocol"); +} catch (error) { + // Root cortex-agent compatibility: the meta/CLI package ships workspace + // sources together, but npm does not create workspace links inside that + // package. Standalone @cortex-agent/sdk installs resolve the package name. + protocol = require("../../protocol/src/index.js"); +} function requireTransport(transport) { if (!transport || typeof transport.query !== "function") { @@ -14,7 +22,9 @@ function requireTransport(transport) { function createCortexClient(options = {}) { const transport = requireTransport(options.transport); - async function query(projection, filters = {}) { + // Deliberately not declared async: local/root CLI transports are synchronous, + // while remote transports may return Promises. Callers can await either. + function queryProjection(projection, filters = {}) { return transport.query(projection, filters); } @@ -28,25 +38,29 @@ function createCortexClient(options = {}) { capabilities: Object.freeze({ discover: typeof transport.discoverCapabilities === "function" ? () => transport.discoverCapabilities() - : async () => ({ protocol_version: protocol.PROTOCOL_VERSION, capabilities: [] }), + : () => ({ protocol_version: protocol.PROTOCOL_VERSION, capabilities: [] }), + }), + management: Object.freeze({ + query: queryProjection, + capabilities: () => queryProjection("capabilities"), }), tasks: Object.freeze({ - get: (taskId) => query("task-state", { task: taskId }), + get: (taskId) => queryProjection("task-state", { task: taskId }), }), runs: Object.freeze({ - list: () => query("runs"), - get: (runId) => query("run-state", { run: runId }), + list: () => queryProjection("runs"), + get: (runId) => queryProjection("run-state", { run: runId }), }), decisions: Object.freeze({ - list: () => query("decisions"), + list: () => queryProjection("decisions"), }), waitpoints: Object.freeze({ - list: () => query("waitpoints"), + list: () => queryProjection("waitpoints"), }), coordination: Object.freeze({ tasks: Object.freeze({ - list: (filters = {}) => query("coordination-tasks", filters), - get: (taskId) => query("coordination-tasks", { task: taskId }), + list: (filters = {}) => queryProjection("coordination-tasks", filters), + get: (taskId) => queryProjection("coordination-tasks", { task: taskId }), }), }), topology: Object.freeze({ From b96dae1acc7cdc8b48a0ae027176ea4b7282a1d7 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:01:19 +0800 Subject: [PATCH 020/166] fix(m040): preserve Management transport exit codes --- lib/sdk/local-transport.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sdk/local-transport.js b/lib/sdk/local-transport.js index 17fffaa9..8f6908da 100644 --- a/lib/sdk/local-transport.js +++ b/lib/sdk/local-transport.js @@ -36,6 +36,7 @@ function createLocalTransport(ctx) { const error = new Error(result.error.message); error.code = result.error.code; error.details = result.error.details; + error.exitCode = result.exitCode; throw error; } return result.project; @@ -57,6 +58,7 @@ function createLocalTransport(ctx) { const error = new Error(result.error.message); error.code = result.error.code; error.details = result.error.details; + error.exitCode = result.exitCode; throw error; } return result.payload; From 18fd775b550ea0b856a1bc422af0e32f2dac84b1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:01:21 +0800 Subject: [PATCH 021/166] refactor(m040): route query CLI through Cortex SDK --- lib/commands/management/query.js | 117 +++++++++++++++++++------------ 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/lib/commands/management/query.js b/lib/commands/management/query.js index d28415db..5708bf76 100644 --- a/lib/commands/management/query.js +++ b/lib/commands/management/query.js @@ -1,62 +1,66 @@ "use strict"; -// ─── query — `cortex-agent query ` CLI surface ──────────────────── -// -// Originally lived inline in lib/commands.js (lines 1393–1488). Extracted so -// that the projection router / capability filter / legacy-fallback logic -// can be unit-tested without spinning up a child process running the -// management-api script. -// -// The function is a strict copy of the original — only the require paths -// change to point at the new sibling module `./api-helpers`. +// `cortex-agent query ` remains a compatibility-stable CLI +// surface. M-040 MS-002 routes its reads through the canonical SDK facade; +// the SDK local transport delegates to the existing Management API owner. -const { - formatQueryPayload, - queryManagementProject, -} = require("../../management/client.js"); +const { formatQueryPayload } = require("../../management/client.js"); +const { createLocalCortexClient } = require("../../sdk/local-client.js"); const { invalidManagementUsage, managementApiError, printManagementPayload, } = require("./api-helpers"); +function sdkFailure(error, fallbackExitCode = 3) { + return { + error: { + code: error && error.code ? error.code : "MANAGEMENT_API_QUERY_FAILED", + message: error && error.message ? error.message : String(error), + details: error && error.details ? error.details : {}, + }, + exitCode: error && error.exitCode ? error.exitCode : fallbackExitCode, + }; +} + function managementQuery(ctx) { const projection = ctx.args[1]; if (!projection || projection.startsWith("--")) { invalidManagementUsage("cortex-agent query [--project ]"); return; } - const capabilityResult = queryManagementProject(ctx, "capabilities"); - if (!capabilityResult.ok) { - // Pre-1.9.0 Management APIs (1.6.0–1.8.x) do not expose a `capabilities` - // projection. Fall through to a direct query so older projects can still - // serve projections the legacy hardcoded dispatcher handled (dashboard-state, - // runs, queues, sessions, inbox, decisions, waitpoints). The Management API - // itself will reject projections it does not know about. - if (capabilityResult.error.code === "UNSUPPORTED_COMMAND") { - const directResult = queryManagementProject(ctx, projection); - if (!directResult.ok) { - managementApiError(ctx, directResult); - return; + + const client = createLocalCortexClient(ctx); + let capabilities; + try { + capabilities = client.management.capabilities(); + } catch (error) { + // Pre-1.9.0 Management APIs (1.6.0–1.8.x) do not expose a capabilities + // projection. Preserve the legacy direct-query fallback. + if (error.code === "UNSUPPORTED_COMMAND") { + try { + const payload = client.management.query(projection); + const project = client.project.resolve(); + printManagementPayload({ + ok: true, + command: "query", + projection, + project: { + root: project.root, + agent_root: project.agent_root, + }, + data: payload, + summary: { legacy_dispatcher: true, capability_filter: "skipped" }, + }); + } catch (directError) { + managementApiError(ctx, sdkFailure(directError)); } - const payload = directResult.payload || {}; - printManagementPayload({ - ok: true, - command: "query", - projection, - project: directResult.project && { - root: directResult.project.root, - agent_root: directResult.project.agent_root, - }, - data: payload, - summary: { legacy_dispatcher: true, capability_filter: "skipped" }, - }); return; } - managementApiError(ctx, capabilityResult); + managementApiError(ctx, sdkFailure(error)); return; } - const capabilities = capabilityResult.payload; + const capability = Array.isArray(capabilities.projections) ? capabilities.projections.find((item) => item && item.name === projection) : null; @@ -74,7 +78,8 @@ function managementQuery(ctx) { }); return; } - const queryArgs = []; + + const filters = {}; for (let index = 2; index < ctx.args.length; index += 1) { const raw = ctx.args[index]; if (raw === "--project") { @@ -102,21 +107,41 @@ function managementQuery(ctx) { const value = equalAt === -1 ? ctx.args[++index] : raw.slice(equalAt + 1); if (!value || value.startsWith("--")) { managementApiError(ctx, { - error: { code: "INVALID_QUERY_OPTION", message: `--${optionName} requires a value.`, details: { option: optionName } }, + error: { + code: "INVALID_QUERY_OPTION", + message: `--${optionName} requires a value.`, + details: { option: optionName }, + }, exitCode: 2, }); return; } - queryArgs.push(`--${optionName}`, value); + if (Object.prototype.hasOwnProperty.call(filters, optionName)) { + filters[optionName] = Array.isArray(filters[optionName]) + ? [...filters[optionName], value] + : [filters[optionName], value]; + } else { + filters[optionName] = value; + } } - const result = queryManagementProject(ctx, projection, queryArgs); - if (!result.ok) { - managementApiError(ctx, result); + + let payload; + let project; + try { + payload = client.management.query(projection, filters); + project = client.project.resolve(); + } catch (error) { + managementApiError(ctx, sdkFailure(error)); return; } - printManagementPayload(formatQueryPayload(result.payload, projection, capability, result.project)); + + printManagementPayload(formatQueryPayload(payload, projection, capability, { + root: project.root, + agent_root: project.agent_root, + })); } module.exports = { managementQuery, + sdkFailure, }; From cd04675c34f97a1d355c1ef6534cabff53d327d3 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:01:24 +0800 Subject: [PATCH 022/166] chore(m040): ship workspace contract sources in root package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 045634a7..0ceedc0a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "files": [ "bin", "lib", + "packages", "scripts", "templates", "hooks", From b20bc61dea425997927b8e9a62378f8e057af6b2 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:02:04 +0800 Subject: [PATCH 023/166] test(m040): guard SDK facade and root package compatibility --- tests/architecture/m040-monorepo-boundary.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/architecture/m040-monorepo-boundary.test.js b/tests/architecture/m040-monorepo-boundary.test.js index 23641786..f69ed485 100644 --- a/tests/architecture/m040-monorepo-boundary.test.js +++ b/tests/architecture/m040-monorepo-boundary.test.js @@ -45,3 +45,17 @@ test("canonical refs are typed opaque strings", () => { assert.equal(refs.isRef("runtime:paseo-local", "runtime"), true); assert.equal(refs.isRef("paseo:raw", "runtime"), false); }); + +test("root cortex-agent package ships workspace contract sources for CLI compatibility", () => { + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")); + assert.ok(pkg.files.includes("packages")); +}); + +test("management query surface consumes the SDK facade rather than Management client directly", () => { + const text = fs.readFileSync( + path.join(ROOT, "lib", "commands", "management", "query.js"), + "utf8", + ); + assert.match(text, /createLocalCortexClient/); + assert.equal(text.includes("queryManagementProject"), false); +}); From 95df4c62726671a5f4061eaf2626aa7921bec9ee Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:02:06 +0800 Subject: [PATCH 024/166] test(m040): cover sync SDK and raw management facade --- tests/sdk/cortex-client.test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/sdk/cortex-client.test.js b/tests/sdk/cortex-client.test.js index 8e7cd6e5..db8ed78c 100644 --- a/tests/sdk/cortex-client.test.js +++ b/tests/sdk/cortex-client.test.js @@ -43,3 +43,34 @@ test("SDK maps canonical methods to existing Management projections", async () = test("SDK fails closed without a query transport", () => { assert.throws(() => createCortexClient(), (error) => error.code === "ERR_SDK_TRANSPORT_REQUIRED"); }); + +test("SDK supports synchronous local transports without forcing async CLI conversion", () => { + const client = createCortexClient({ + transport: { + query(projection, filters = {}) { + return { projection, filters }; + }, + }, + }); + const result = client.tasks.get("T-SYNC"); + assert.equal(typeof result.then, "undefined"); + assert.deepEqual(result, { projection: "task-state", filters: { task: "T-SYNC" } }); +}); + +test("management namespace exposes raw projection compatibility through the SDK", () => { + const calls = []; + const client = createCortexClient({ + transport: { + query(projection, filters = {}) { + calls.push({ projection, filters }); + return { ok: true, projection }; + }, + }, + }); + assert.equal(client.management.capabilities().projection, "capabilities"); + assert.equal(client.management.query("runs").projection, "runs"); + assert.deepEqual(calls, [ + { projection: "capabilities", filters: {} }, + { projection: "runs", filters: {} }, + ]); +}); From e106a71593766fc6e4f555dccad9734061bdcfbe Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:02:08 +0800 Subject: [PATCH 025/166] test(m040): include management query regression suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ceedc0a..a0c64369 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/management/management-query-cli.test.js" }, "keywords": [ "ai", From 0210894c1adee20b71b81a86f78d2e1ea48b5aff Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:02:43 +0800 Subject: [PATCH 026/166] feat(m040): resolve stable project identity through topology --- lib/sdk/local-transport.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/sdk/local-transport.js b/lib/sdk/local-transport.js index 8f6908da..42e184ae 100644 --- a/lib/sdk/local-transport.js +++ b/lib/sdk/local-transport.js @@ -42,17 +42,30 @@ function createLocalTransport(ctx) { return result.project; } + function readProjectIdentity(project) { + const current = topology.readTopology(project.root); + const projectId = current && current.self && typeof current.self.project_id === "string" + ? current.self.project_id.trim() + : ""; + return { + project_id: projectId || path.basename(project.root), + topology: current, + }; + } + return Object.freeze({ - async resolveProject() { + resolveProject() { const project = resolve(); + const identity = readProjectIdentity(project); return { - project_ref: `project:${path.basename(project.root)}`, + project_ref: `project:${identity.project_id}`, + project_id: identity.project_id, root: project.root, agent_root: project.agent_root, }; }, - async query(projection, filters = {}) { + query(projection, filters = {}) { const result = queryManagementProject(ctx, projection, filtersToArgs(filters)); if (!result.ok) { const error = new Error(result.error.message); @@ -64,13 +77,14 @@ function createLocalTransport(ctx) { return result.payload; }, - async getTopology() { + getTopology() { const project = resolve(); return topology.readTopology(project.root); }, - async discoverCapabilities() { + discoverCapabilities() { return { + schema_version: "1.0", protocol: "cortex", protocol_version: "1.0", transport: "local", From 4914d0085c7762b8db03585eeda3b366feadc9ca Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:02:46 +0800 Subject: [PATCH 027/166] feat(m040): add canonical capability identifiers --- packages/protocol/src/capabilities.js | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/protocol/src/capabilities.js diff --git a/packages/protocol/src/capabilities.js b/packages/protocol/src/capabilities.js new file mode 100644 index 00000000..42575204 --- /dev/null +++ b/packages/protocol/src/capabilities.js @@ -0,0 +1,66 @@ +"use strict"; + +const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/; + +const CAPABILITY_NAMESPACES = Object.freeze([ + "management", + "tasks", + "runs", + "decisions", + "waitpoints", + "coordination", + "topology", + "runtime", + "project", + "extension", +]); + +const CAPABILITY_NAMESPACE_SET = new Set(CAPABILITY_NAMESPACES); + +function parseCapabilityId(value) { + if (typeof value !== "string" || !CAPABILITY_ID_PATTERN.test(value)) return null; + const [namespace, ...segments] = value.split("."); + if (!CAPABILITY_NAMESPACE_SET.has(namespace)) return null; + return Object.freeze({ + id: value, + namespace, + segments: Object.freeze(segments), + }); +} + +function isCapabilityId(value, namespace) { + const parsed = parseCapabilityId(value); + if (!parsed) return false; + return !namespace || parsed.namespace === namespace; +} + +function validateCapabilityList(values) { + if (!Array.isArray(values)) { + const error = new Error("Capability list must be an array."); + error.code = "ERR_CAPABILITY_LIST_INVALID"; + throw error; + } + const normalized = []; + const seen = new Set(); + for (const value of values) { + const parsed = parseCapabilityId(value); + if (!parsed) { + const error = new Error(`Invalid Cortex capability id: ${value}`); + error.code = "ERR_CAPABILITY_ID_INVALID"; + error.details = { capability: value }; + throw error; + } + if (!seen.has(parsed.id)) { + seen.add(parsed.id); + normalized.push(parsed.id); + } + } + return Object.freeze(normalized); +} + +module.exports = { + CAPABILITY_NAMESPACES, + parseCapabilityId, + isCapabilityId, + validateCapabilityList, +}; From 4778d48dbf2ea588f2045528488cc7e7598346f0 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:02:48 +0800 Subject: [PATCH 028/166] feat(m040): export capability contract primitives --- packages/protocol/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/src/index.js b/packages/protocol/src/index.js index 481de126..863321f6 100644 --- a/packages/protocol/src/index.js +++ b/packages/protocol/src/index.js @@ -4,4 +4,5 @@ module.exports = { ...require("./refs"), ...require("./version"), ...require("./result"), + ...require("./capabilities"), }; From 22cbb3a1e7261ba526fbf1f613aa3fbfeadb878c Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:03:45 +0800 Subject: [PATCH 029/166] test(m040): add capability contract tests --- tests/protocol/capabilities.test.js | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/protocol/capabilities.test.js diff --git a/tests/protocol/capabilities.test.js b/tests/protocol/capabilities.test.js new file mode 100644 index 00000000..6d730f32 --- /dev/null +++ b/tests/protocol/capabilities.test.js @@ -0,0 +1,39 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + CAPABILITY_NAMESPACES, + parseCapabilityId, + isCapabilityId, + validateCapabilityList, +} = require(path.join(ROOT, "packages", "protocol", "src", "capabilities.js")); + +test("canonical capability ids are namespaced and closed to known namespaces", () => { + assert.ok(CAPABILITY_NAMESPACES.includes("runtime")); + assert.deepEqual(parseCapabilityId("runtime.run.create"), { + id: "runtime.run.create", + namespace: "runtime", + segments: ["run", "create"], + }); + assert.equal(parseCapabilityId("paseo.run.create"), null); + assert.equal(parseCapabilityId("runtime"), null); + assert.equal(isCapabilityId("topology.read", "topology"), true); +}); + +test("capability lists are deduplicated without reordering", () => { + assert.deepEqual( + validateCapabilityList(["runs.read", "tasks.read", "runs.read"]), + ["runs.read", "tasks.read"], + ); +}); + +test("invalid capability ids fail closed", () => { + assert.throws( + () => validateCapabilityList(["runtime.run.create", "Paseo.Run"]), + (error) => error.code === "ERR_CAPABILITY_ID_INVALID", + ); +}); From c9227f37a7e54affddbfcd5e741c2f1fbd7c4512 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:03:48 +0800 Subject: [PATCH 030/166] test(m040): guard topology-derived ProjectRef identity --- tests/architecture/m040-monorepo-boundary.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/architecture/m040-monorepo-boundary.test.js b/tests/architecture/m040-monorepo-boundary.test.js index f69ed485..7869d33d 100644 --- a/tests/architecture/m040-monorepo-boundary.test.js +++ b/tests/architecture/m040-monorepo-boundary.test.js @@ -59,3 +59,13 @@ test("management query surface consumes the SDK facade rather than Management cl assert.match(text, /createLocalCortexClient/); assert.equal(text.includes("queryManagementProject"), false); }); + +test("ProjectRef identity is derived from topology before directory basename fallback", () => { + const text = fs.readFileSync( + path.join(ROOT, "lib", "sdk", "local-transport.js"), + "utf8", + ); + assert.match(text, /current\.self\.project_id/); + assert.match(text, /path\.basename\(project\.root\)/); + assert.ok(text.indexOf("current.self.project_id") < text.indexOf("path.basename(project.root)")); +}); From 0a9cbf67733d8284a0aee8458dd8fd491e8d7a9e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:03:50 +0800 Subject: [PATCH 031/166] test(m040): include protocol capability suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0c64369..c9a69eb1 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/management/management-query-cli.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/management/management-query-cli.test.js" }, "keywords": [ "ai", From d401ef297c81c593f4b22678b197274a43fb485a Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:04:12 +0800 Subject: [PATCH 032/166] ci(m040): validate protocol sdk workspace --- .github/workflows/m040-validation.yml | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/m040-validation.yml diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml new file mode 100644 index 00000000..a079b6ef --- /dev/null +++ b/.github/workflows/m040-validation.yml @@ -0,0 +1,56 @@ +name: M-040 Architecture Validation + +on: + push: + branches: + - "feat/m040-*" + paths: + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - "packages/**" + - "lib/sdk/**" + - "lib/commands/management/query.js" + - "tests/architecture/**" + - "tests/protocol/**" + - "tests/sdk/**" + - "tests/management/management-query-cli.test.js" + - ".github/workflows/m040-validation.yml" + pull_request: + paths: + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - "packages/**" + - "lib/sdk/**" + - "lib/commands/management/query.js" + - "tests/architecture/**" + - "tests/protocol/**" + - "tests/sdk/**" + - "tests/management/management-query-cli.test.js" + - ".github/workflows/m040-validation.yml" + +permissions: + contents: read + +jobs: + protocol-sdk: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24.19.0" + cache: "pnpm" + + - name: Enable Corepack + run: corepack enable + + - name: Install workspace + run: pnpm install --frozen-lockfile + + - name: Run M-040 focused validation + run: pnpm test:m040 From 480bf5c0c10e196a6eb4180ea0314a91ff7bc094 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:04:50 +0800 Subject: [PATCH 033/166] ci(m040): activate pnpm before workspace install --- .github/workflows/m040-validation.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml index a079b6ef..8d00dc98 100644 --- a/.github/workflows/m040-validation.yml +++ b/.github/workflows/m040-validation.yml @@ -44,10 +44,12 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "24.19.0" - cache: "pnpm" - - name: Enable Corepack - run: corepack enable + - name: Enable Corepack and pnpm + run: | + corepack enable + corepack prepare pnpm@11.13.1 --activate + pnpm --version - name: Install workspace run: pnpm install --frozen-lockfile From cdcfd76f6ad2bd91f179a6519166bb679458b4c7 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:06:13 +0800 Subject: [PATCH 034/166] docs: document M-040 protocol sdk monorepo baseline --- docs/architecture/protocol-sdk-monorepo.md | 188 +++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/architecture/protocol-sdk-monorepo.md diff --git a/docs/architecture/protocol-sdk-monorepo.md b/docs/architecture/protocol-sdk-monorepo.md new file mode 100644 index 00000000..07c076ef --- /dev/null +++ b/docs/architecture/protocol-sdk-monorepo.md @@ -0,0 +1,188 @@ +# Protocol, SDK and Progressive Monorepo + +> **Status**: M-040 MS-002 baseline +> **Validated**: 2026-09-29 +> **CI**: M-040 Architecture Validation run 36519997237 — PASS + +## 1. Purpose + +Cortex Agent now uses a progressive pnpm workspace to isolate stable contracts and SDK surfaces without moving existing authoritative implementation owners out of `lib/` prematurely. + +The root `cortex-agent` package remains the primary CLI/template distribution package. + +## 2. Workspace layout + +```text +cortex-agent/ +├── package.json +├── pnpm-workspace.yaml +├── pnpm-lock.yaml +├── packages/ +│ ├── protocol/ +│ └── sdk/ +├── lib/ +├── bin/ +├── templates/ +└── docs/ +``` + +Future M-040 packages may include: + +```text +packages/runtime-port/ +packages/project-sdk/ +packages/extension-sdk/ +``` + +They are not introduced until their milestone boundaries are ready. + +## 3. @cortex-agent/protocol + +The protocol package is portable and implementation-free. + +Current responsibilities: + +- opaque typed refs; +- protocol version constants; +- canonical result/error envelope; +- canonical capability identifiers/namespaces. + +Current ref kinds: + +```text +project +workspace +run +agent +host +runtime +session +``` + +Examples: + +```text +project:axrail +runtime:paseo-local +host:mac-mini +``` + +The protocol package MUST NOT depend on filesystem, child_process, Management implementation, UI, Paseo, Axrail, or provider SDKs. + +## 4. @cortex-agent/sdk + +The SDK is a stable consumer facade and owns no persistence. + +Current read-first surface: + +```text +client.project.resolve() +client.capabilities.discover() + +client.management.query() +client.management.capabilities() + +client.tasks.get() + +client.runs.list() +client.runs.get() + +client.decisions.list() +client.waitpoints.list() + +client.coordination.tasks.list() +client.coordination.tasks.get() + +client.topology.get() +``` + +The SDK transport may be synchronous or asynchronous. This preserves the existing synchronous CLI while allowing future remote transports. + +## 5. Local transport + +`lib/sdk/local-transport.js` bridges the SDK to existing authoritative owners: + +```text +SDK + └─ local transport + ├─ Management API + └─ Topology +``` + +It does not write a new state store. + +Project identity is resolved from topology `self.project_id` first, with repository directory basename only as a compatibility fallback. + +## 6. First migrated public surface + +`cortex-agent query ` now routes reads through: + +```text +CLI query + -> createLocalCortexClient() + -> SDK management facade + -> local transport + -> Management API +``` + +Compatibility preserved: + +- projection filtering; +- `--project`; +- exact projection filters; +- output envelope; +- exit-code behavior; +- legacy projects without the capabilities projection. + +## 7. Capability identifier model + +Canonical capability IDs are namespaced strings. + +Examples: + +```text +management.query +tasks.read +runs.read +topology.read +runtime.run.create +project.validation.run +extension.event.consume +``` + +Current top-level namespaces include management, tasks, runs, decisions, waitpoints, coordination, topology, runtime, project, and extension. + +M-040 MS-003 extends this with protocol-version negotiation and compatibility rules. + +## 8. Packaging compatibility + +The root package includes `packages/` in its npm artifact so internal SDK fallback imports work without npm workspace links. + +Standalone workspace packages can use normal package dependencies. + +The root package remains installable as one package. + +## 9. Validation + +The focused validation workflow verifies: + +1. Node 24.19.0 +2. pnpm 11.13.1 +3. `pnpm install --frozen-lockfile` +4. architecture boundary tests +5. capability contract tests +6. SDK tests +7. local transport tests +8. existing Management query CLI regression tests + +Validated result: + +```text +25 tests +25 pass +0 fail +``` + +## 10. Non-goals + +MS-002 does not move all `lib/` implementation into packages, publish every workspace package, introduce a second persistence model, create RuntimePort, create Project SDK, create Extension SDK, or introduce Nx/Turbo/Lerna. From edaba9e81bea3f7e973697483802f45ffa9670f5 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:06:15 +0800 Subject: [PATCH 035/166] docs: index M-040 protocol sdk architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 05fca4d2..dfd5db4d 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -27,6 +27,7 @@ - [Agent Workspace Orchestration](./agent-workspace-orchestration.md) - [Agent Runtime Continuity](./agent-runtime-continuity.md) - [Runtime, Control and Integration Ownership](./runtime-control-ownership.md) +- [Protocol, SDK and Progressive Monorepo](./protocol-sdk-monorepo.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 28ab8735015cb45ffb9608ac487a0388c97b1484 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:07:51 +0800 Subject: [PATCH 036/166] feat(m040): define protocol families --- packages/protocol/src/version.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/protocol/src/version.js b/packages/protocol/src/version.js index 2eea21ea..f430ade4 100644 --- a/packages/protocol/src/version.js +++ b/packages/protocol/src/version.js @@ -2,11 +2,23 @@ const PROTOCOL_NAME = "cortex"; const PROTOCOL_VERSION = "1.0"; + const RUNTIME_PROTOCOL_NAME = "cortex-runtime"; const RUNTIME_PROTOCOL_VERSION = "1.0"; + const PROJECT_PROTOCOL_NAME = "cortex-project"; const PROJECT_PROTOCOL_VERSION = "1.0"; +const EXTENSION_PROTOCOL_NAME = "cortex-extension"; +const EXTENSION_PROTOCOL_VERSION = "1.0"; + +const PROTOCOL_CURRENT = Object.freeze({ + [PROTOCOL_NAME]: PROTOCOL_VERSION, + [RUNTIME_PROTOCOL_NAME]: RUNTIME_PROTOCOL_VERSION, + [PROJECT_PROTOCOL_NAME]: PROJECT_PROTOCOL_VERSION, + [EXTENSION_PROTOCOL_NAME]: EXTENSION_PROTOCOL_VERSION, +}); + module.exports = { PROTOCOL_NAME, PROTOCOL_VERSION, @@ -14,4 +26,7 @@ module.exports = { RUNTIME_PROTOCOL_VERSION, PROJECT_PROTOCOL_NAME, PROJECT_PROTOCOL_VERSION, + EXTENSION_PROTOCOL_NAME, + EXTENSION_PROTOCOL_VERSION, + PROTOCOL_CURRENT, }; From e66d1f114946c2549a145563207da1d66e69ce0d Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:07:53 +0800 Subject: [PATCH 037/166] feat(m040): add protocol capability negotiation --- packages/protocol/src/negotiation.js | 212 +++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 packages/protocol/src/negotiation.js diff --git a/packages/protocol/src/negotiation.js b/packages/protocol/src/negotiation.js new file mode 100644 index 00000000..6db54b05 --- /dev/null +++ b/packages/protocol/src/negotiation.js @@ -0,0 +1,212 @@ +"use strict"; + +const { PROTOCOL_CURRENT } = require("./version"); +const { validateCapabilityList } = require("./capabilities"); + +const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)$/; +const DESCRIPTOR_SCHEMA_VERSION = "1"; +const KNOWN_DESCRIPTOR_KEYS = new Set([ + "schema_version", + "protocol", + "protocol_version", + "implementation", + "implementation_version", + "capabilities", + "deprecated_capabilities", +]); +const KNOWN_DEPRECATION_KEYS = new Set([ + "id", + "replacement", + "remove_in", + "reason", +]); + +class ProtocolNegotiationError extends Error { + constructor(code, details = {}) { + super(`[protocol-negotiation:${code}] ${JSON.stringify(details)}`); + this.name = "ProtocolNegotiationError"; + this.code = code; + this.details = details; + } +} + +function parseProtocolVersion(value) { + if (typeof value !== "string") return null; + const match = VERSION_PATTERN.exec(value); + if (!match) return null; + return Object.freeze({ + raw: value, + major: Number(match[1]), + minor: Number(match[2]), + }); +} + +function compareProtocolVersions(left, right) { + const a = typeof left === "string" ? parseProtocolVersion(left) : left; + const b = typeof right === "string" ? parseProtocolVersion(right) : right; + if (!a || !b) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_VERSION_INVALID", { left, right }); + } + if (a.major !== b.major) return a.major < b.major ? -1 : 1; + if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1; + return 0; +} + +function rejectUnknownKeys(value, known, where) { + for (const key of Object.keys(value || {})) { + if (!known.has(key)) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_FIELD_UNKNOWN", { where, key }); + } + } +} + +function nonEmpty(value, where) { + if (typeof value !== "string" || !value.trim()) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_FIELD_INVALID", { where }); + } + return value.trim(); +} + +function normalizeDeprecations(value, capabilities) { + if (value === undefined) return Object.freeze([]); + if (!Array.isArray(value)) { + throw new ProtocolNegotiationError("ERR_DEPRECATIONS_INVALID", {}); + } + const capabilitySet = new Set(capabilities); + const out = []; + const seen = new Set(); + for (const item of value) { + if (!item || typeof item !== "object" || Array.isArray(item)) { + throw new ProtocolNegotiationError("ERR_DEPRECATION_INVALID", { item }); + } + rejectUnknownKeys(item, KNOWN_DEPRECATION_KEYS, "deprecated_capabilities[]"); + const id = nonEmpty(item.id, "deprecated_capabilities[].id"); + if (!capabilitySet.has(id)) { + throw new ProtocolNegotiationError("ERR_DEPRECATION_CAPABILITY_NOT_PROVIDED", { id }); + } + if (seen.has(id)) { + throw new ProtocolNegotiationError("ERR_DEPRECATION_DUPLICATE", { id }); + } + seen.add(id); + const replacement = item.replacement == null ? null : nonEmpty(item.replacement, "deprecated_capabilities[].replacement"); + if (replacement) validateCapabilityList([replacement]); + const removeIn = item.remove_in == null ? null : nonEmpty(item.remove_in, "deprecated_capabilities[].remove_in"); + if (removeIn && !parseProtocolVersion(removeIn)) { + throw new ProtocolNegotiationError("ERR_DEPRECATION_REMOVE_VERSION_INVALID", { id, remove_in: removeIn }); + } + out.push(Object.freeze({ + id, + replacement, + remove_in: removeIn, + reason: item.reason == null ? null : nonEmpty(item.reason, "deprecated_capabilities[].reason"), + })); + } + return Object.freeze(out); +} + +function createCapabilityDescriptor(input) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new ProtocolNegotiationError("ERR_DESCRIPTOR_INVALID", {}); + } + rejectUnknownKeys(input, KNOWN_DESCRIPTOR_KEYS, "descriptor"); + + const schemaVersion = input.schema_version == null ? DESCRIPTOR_SCHEMA_VERSION : String(input.schema_version); + if (schemaVersion !== DESCRIPTOR_SCHEMA_VERSION) { + throw new ProtocolNegotiationError("ERR_DESCRIPTOR_SCHEMA_VERSION", { + expected: DESCRIPTOR_SCHEMA_VERSION, + received: schemaVersion, + }); + } + + const protocol = nonEmpty(input.protocol, "protocol"); + if (!Object.prototype.hasOwnProperty.call(PROTOCOL_CURRENT, protocol)) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_UNKNOWN", { protocol }); + } + + const protocolVersion = nonEmpty(input.protocol_version, "protocol_version"); + if (!parseProtocolVersion(protocolVersion)) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_VERSION_INVALID", { protocol_version: protocolVersion }); + } + + const capabilities = validateCapabilityList(input.capabilities || []); + const deprecated = normalizeDeprecations(input.deprecated_capabilities, capabilities); + + return Object.freeze({ + schema_version: DESCRIPTOR_SCHEMA_VERSION, + protocol, + protocol_version: protocolVersion, + implementation: nonEmpty(input.implementation, "implementation"), + implementation_version: input.implementation_version == null + ? null + : nonEmpty(input.implementation_version, "implementation_version"), + capabilities, + deprecated_capabilities: deprecated, + }); +} + +function negotiateProtocol(localInput, remoteInput, options = {}) { + const local = createCapabilityDescriptor(localInput); + const remote = createCapabilityDescriptor(remoteInput); + + if (local.protocol !== remote.protocol) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_NAME_MISMATCH", { + local: local.protocol, + remote: remote.protocol, + }); + } + + const localVersion = parseProtocolVersion(local.protocol_version); + const remoteVersion = parseProtocolVersion(remote.protocol_version); + if (localVersion.major !== remoteVersion.major) { + throw new ProtocolNegotiationError("ERR_PROTOCOL_MAJOR_MISMATCH", { + local: local.protocol_version, + remote: remote.protocol_version, + }); + } + + const remoteCaps = new Set(remote.capabilities); + const common = local.capabilities.filter((capability) => remoteCaps.has(capability)); + const required = validateCapabilityList(options.required_capabilities || []); + const commonSet = new Set(common); + const missing = required.filter((capability) => !commonSet.has(capability)); + if (missing.length) { + throw new ProtocolNegotiationError("ERR_REQUIRED_CAPABILITY_MISSING", { + missing, + common, + }); + } + + const negotiatedMinor = Math.min(localVersion.minor, remoteVersion.minor); + const deprecations = []; + for (const source of [ + ["local", local.deprecated_capabilities], + ["remote", remote.deprecated_capabilities], + ]) { + for (const item of source[1]) { + if (commonSet.has(item.id)) { + deprecations.push(Object.freeze({ source: source[0], ...item })); + } + } + } + + return Object.freeze({ + protocol: local.protocol, + negotiated_version: `${localVersion.major}.${negotiatedMinor}`, + local_version: local.protocol_version, + remote_version: remote.protocol_version, + local_newer: compareProtocolVersions(localVersion, remoteVersion) > 0, + remote_newer: compareProtocolVersions(localVersion, remoteVersion) < 0, + capabilities: Object.freeze(common), + required_capabilities: required, + deprecated_capabilities: Object.freeze(deprecations), + }); +} + +module.exports = { + DESCRIPTOR_SCHEMA_VERSION, + ProtocolNegotiationError, + parseProtocolVersion, + compareProtocolVersions, + createCapabilityDescriptor, + negotiateProtocol, +}; From f55eba7e652e6b0ff4de12558d3824515cfdb971 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:07:56 +0800 Subject: [PATCH 038/166] feat(m040): export protocol negotiation --- packages/protocol/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/src/index.js b/packages/protocol/src/index.js index 863321f6..ce08a7e0 100644 --- a/packages/protocol/src/index.js +++ b/packages/protocol/src/index.js @@ -5,4 +5,5 @@ module.exports = { ...require("./version"), ...require("./result"), ...require("./capabilities"), + ...require("./negotiation"), }; From e58e9a71cb202f1d1c5d0379b06ee6e10cbbd9a5 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:07:58 +0800 Subject: [PATCH 039/166] test(m040): cover protocol negotiation --- tests/protocol/negotiation.test.js | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/protocol/negotiation.test.js diff --git a/tests/protocol/negotiation.test.js b/tests/protocol/negotiation.test.js new file mode 100644 index 00000000..85f7d006 --- /dev/null +++ b/tests/protocol/negotiation.test.js @@ -0,0 +1,90 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const protocol = require(path.join(ROOT, "packages", "protocol", "src")); + +function descriptor(overrides = {}) { + return { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: "test-runtime", + implementation_version: "0.1.0", + capabilities: [ + "runtime.run.create", + "runtime.run.cancel", + "runtime.timeline.read", + ], + ...overrides, + }; +} + +test("protocol versions parse and compare deterministically", () => { + assert.deepEqual(protocol.parseProtocolVersion("1.12"), { raw: "1.12", major: 1, minor: 12 }); + assert.equal(protocol.parseProtocolVersion("v1.0"), null); + assert.equal(protocol.compareProtocolVersions("1.2", "1.3"), -1); + assert.equal(protocol.compareProtocolVersions("2.0", "1.99"), 1); +}); + +test("same-major negotiation uses the lower common minor and capability intersection", () => { + const result = protocol.negotiateProtocol( + descriptor({ protocol_version: "1.3" }), + descriptor({ + protocol_version: "1.1", + implementation: "remote", + capabilities: ["runtime.run.create", "runtime.timeline.read"], + }), + { required_capabilities: ["runtime.run.create"] }, + ); + assert.equal(result.negotiated_version, "1.1"); + assert.equal(result.local_newer, true); + assert.deepEqual(result.capabilities, ["runtime.run.create", "runtime.timeline.read"]); +}); + +test("major version mismatch fails closed", () => { + assert.throws( + () => protocol.negotiateProtocol( + descriptor({ protocol_version: "1.4" }), + descriptor({ protocol_version: "2.0", implementation: "remote" }), + ), + (error) => error.code === "ERR_PROTOCOL_MAJOR_MISMATCH", + ); +}); + +test("missing required capability fails closed", () => { + assert.throws( + () => protocol.negotiateProtocol( + descriptor(), + descriptor({ implementation: "remote", capabilities: ["runtime.timeline.read"] }), + { required_capabilities: ["runtime.run.cancel"] }, + ), + (error) => error.code === "ERR_REQUIRED_CAPABILITY_MISSING" + && error.details.missing.includes("runtime.run.cancel"), + ); +}); + +test("deprecations are explicit metadata and only apply to provided capabilities", () => { + const local = descriptor({ + deprecated_capabilities: [{ + id: "runtime.run.cancel", + replacement: "runtime.run.stop", + remove_in: "2.0", + reason: "unify stop semantics", + }], + }); + const remote = descriptor({ implementation: "remote" }); + const result = protocol.negotiateProtocol(local, remote); + assert.equal(result.deprecated_capabilities.length, 1); + assert.equal(result.deprecated_capabilities[0].source, "local"); + assert.equal(result.deprecated_capabilities[0].replacement, "runtime.run.stop"); +}); + +test("descriptor rejects unknown protocol fields", () => { + assert.throws( + () => protocol.createCapabilityDescriptor({ ...descriptor(), vendor_magic: true }), + (error) => error.code === "ERR_PROTOCOL_FIELD_UNKNOWN", + ); +}); From 5089296fe47d20907e86009d0a6cb93b0cce6a36 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:08:34 +0800 Subject: [PATCH 040/166] feat(m040): validate and negotiate SDK capabilities --- packages/sdk/src/index.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/index.js b/packages/sdk/src/index.js index 1dc0d85b..e0988493 100644 --- a/packages/sdk/src/index.js +++ b/packages/sdk/src/index.js @@ -19,6 +19,13 @@ function requireTransport(transport) { return transport; } +function normalizeDescriptor(value) { + if (value && typeof value.then === "function") { + return value.then((resolved) => protocol.createCapabilityDescriptor(resolved)); + } + return protocol.createCapabilityDescriptor(value); +} + function createCortexClient(options = {}) { const transport = requireTransport(options.transport); @@ -28,6 +35,19 @@ function createCortexClient(options = {}) { return transport.query(projection, filters); } + function discoverCapabilities() { + const raw = typeof transport.discoverCapabilities === "function" + ? transport.discoverCapabilities() + : { + protocol: protocol.PROTOCOL_NAME, + protocol_version: protocol.PROTOCOL_VERSION, + implementation: "cortex-sdk-transport", + implementation_version: null, + capabilities: [], + }; + return normalizeDescriptor(raw); + } + return Object.freeze({ protocol, project: Object.freeze({ @@ -36,9 +56,14 @@ function createCortexClient(options = {}) { : undefined, }), capabilities: Object.freeze({ - discover: typeof transport.discoverCapabilities === "function" - ? () => transport.discoverCapabilities() - : () => ({ protocol_version: protocol.PROTOCOL_VERSION, capabilities: [] }), + discover: discoverCapabilities, + negotiate: (remoteDescriptor, options = {}) => { + const local = discoverCapabilities(); + if (local && typeof local.then === "function") { + return local.then((resolved) => protocol.negotiateProtocol(resolved, remoteDescriptor, options)); + } + return protocol.negotiateProtocol(local, remoteDescriptor, options); + }, }), management: Object.freeze({ query: queryProjection, From 24fa88c7d2dfdc813a9985f571d676ab57828752 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:08:36 +0800 Subject: [PATCH 041/166] feat(m040): emit standard local capability descriptor --- lib/sdk/local-transport.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/sdk/local-transport.js b/lib/sdk/local-transport.js index 42e184ae..86734284 100644 --- a/lib/sdk/local-transport.js +++ b/lib/sdk/local-transport.js @@ -84,10 +84,11 @@ function createLocalTransport(ctx) { discoverCapabilities() { return { - schema_version: "1.0", + schema_version: "1", protocol: "cortex", protocol_version: "1.0", - transport: "local", + implementation: "cortex-local-transport", + implementation_version: null, capabilities: [ "management.query", "tasks.read", From b69d30d1cd292e356a48e8d8505193cf4d53c66e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:08:39 +0800 Subject: [PATCH 042/166] test(m040): include protocol negotiation suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9a69eb1..5d60d695 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/management/management-query-cli.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/management/management-query-cli.test.js" }, "keywords": [ "ai", From 066c8b875af8274212fb721a45128a816636483e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:09:30 +0800 Subject: [PATCH 043/166] test(m040): cover SDK capability negotiation --- tests/sdk/cortex-client.test.js | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/sdk/cortex-client.test.js b/tests/sdk/cortex-client.test.js index db8ed78c..502f9e11 100644 --- a/tests/sdk/cortex-client.test.js +++ b/tests/sdk/cortex-client.test.js @@ -74,3 +74,43 @@ test("management namespace exposes raw projection compatibility through the SDK" { projection: "runs", filters: {} }, ]); }); + +test("SDK capability negotiation uses the local descriptor and fails closed on missing requirements", () => { + const client = createCortexClient({ + transport: { + query() { return {}; }, + discoverCapabilities() { + return { + protocol: "cortex", + protocol_version: "1.2", + implementation: "local-test", + capabilities: ["runs.read", "tasks.read"], + }; + }, + }, + }); + + const negotiated = client.capabilities.negotiate({ + protocol: "cortex", + protocol_version: "1.1", + implementation: "remote-test", + capabilities: ["runs.read", "decisions.read"], + }, { + required_capabilities: ["runs.read"], + }); + + assert.equal(negotiated.negotiated_version, "1.1"); + assert.deepEqual(negotiated.capabilities, ["runs.read"]); + + assert.throws( + () => client.capabilities.negotiate({ + protocol: "cortex", + protocol_version: "1.1", + implementation: "remote-test", + capabilities: ["decisions.read"], + }, { + required_capabilities: ["runs.read"], + }), + (error) => error.code === "ERR_REQUIRED_CAPABILITY_MISSING", + ); +}); From fa22420cf2ecd8e5fd8f6e758dcb13ea494b4b07 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:09:33 +0800 Subject: [PATCH 044/166] test(m040): validate local capability descriptor shape --- tests/sdk/local-transport.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/sdk/local-transport.test.js b/tests/sdk/local-transport.test.js index 80ef3597..d13b2cc0 100644 --- a/tests/sdk/local-transport.test.js +++ b/tests/sdk/local-transport.test.js @@ -26,3 +26,14 @@ test("local SDK transport repeats array-valued filters deterministically", () => "--host", "claude-code", ]); }); + +test("local transport advertises a standard Cortex capability descriptor shape", () => { + const text = require("node:fs").readFileSync( + path.join(ROOT, "lib", "sdk", "local-transport.js"), + "utf8", + ); + assert.match(text, /schema_version:\s*"1"/); + assert.match(text, /implementation:\s*"cortex-local-transport"/); + assert.match(text, /protocol_version:\s*"1\.0"/); + assert.equal(text.includes('transport: "local"'), false); +}); From 98ab3e14c1e0eb9b47cc6a29911886791f63791a Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:10:05 +0800 Subject: [PATCH 045/166] docs: define M-040 protocol negotiation --- docs/architecture/protocol-negotiation.md | 221 ++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 docs/architecture/protocol-negotiation.md diff --git a/docs/architecture/protocol-negotiation.md b/docs/architecture/protocol-negotiation.md new file mode 100644 index 00000000..575f0183 --- /dev/null +++ b/docs/architecture/protocol-negotiation.md @@ -0,0 +1,221 @@ +# Protocol Versioning and Capability Negotiation + +> **Status**: M-040 MS-003 baseline +> **Date**: 2026-09-29 + +## 1. Protocol families + +Cortex separates protocol families by integration boundary: + +```text +cortex general SDK / management capabilities +cortex-runtime RuntimePort integration +cortex-project external Project Integration +cortex-extension extension/plugin integration +``` + +Each family currently starts at `1.0`. + +## 2. Version model + +Protocol versions use: + +```text +major.minor +``` + +Rules: + +- same major may negotiate; +- negotiated version is the lower common minor; +- minor evolution must be additive; +- major mismatch fails closed; +- no implicit downgrade across majors; +- timestamps or implementation versions do not affect protocol compatibility. + +Example: + +```text +local 1.3 +remote 1.1 +=> negotiated 1.1 + +local 1.3 +remote 2.0 +=> fail: ERR_PROTOCOL_MAJOR_MISMATCH +``` + +## 3. Capability descriptor + +Canonical descriptor shape: + +```json +{ + "schema_version": "1", + "protocol": "cortex-runtime", + "protocol_version": "1.0", + "implementation": "paseo", + "implementation_version": "0.x", + "capabilities": [ + "runtime.run.create", + "runtime.run.cancel", + "runtime.timeline.read" + ], + "deprecated_capabilities": [] +} +``` + +The descriptor is intentionally small and stable. + +Unknown descriptor fields fail closed so one major version cannot silently reinterpret a different contract. + +## 4. Capability identifiers + +Canonical capability IDs are namespaced strings. + +Examples: + +```text +management.query +tasks.read +runs.read +runtime.run.create +runtime.timeline.read +project.validation.run +extension.event.consume +``` + +Current namespaces: + +- management +- tasks +- runs +- decisions +- waitpoints +- coordination +- topology +- runtime +- project +- extension + +The existing Host Capability Descriptor under `lib/runtime-adapters/capability-contract.js` remains separate for now. Host observability names such as `tool.before.block` are not forced into the new generic namespace until the later Host/Runtime topology milestone defines the bridge. + +## 5. Negotiation + +Negotiation computes: + +```text +protocol compatibility ++ +capability intersection ++ +required-capability validation ++ +deprecation metadata +``` + +Example: + +```text +local: + runs.read + tasks.read + +remote: + runs.read + decisions.read + +common: + runs.read +``` + +If the caller requires `tasks.read`, negotiation fails. + +A routing or score result must never manufacture a missing capability. + +## 6. Required vs optional capabilities + +Required capabilities are hard filters. + +Optional capabilities may be absent without failing negotiation. + +This rule is important for graceful degradation: + +```text +required capability missing +=> fail closed + +optional capability missing +=> feature unavailable, continue +``` + +## 7. Deprecation + +A descriptor may mark one of its provided capabilities as deprecated. + +Metadata may include: + +- replacement capability; +- removal protocol version; +- reason. + +Deprecation is advisory compatibility metadata. It does not automatically enable the replacement capability. + +Example: + +```json +{ + "id": "runtime.run.cancel", + "replacement": "runtime.run.stop", + "remove_in": "2.0", + "reason": "unify stop semantics" +} +``` + +## 8. SDK integration + +The SDK validates capability discovery responses through the protocol package. + +```text +transport.discoverCapabilities() + | + v +createCapabilityDescriptor() + | + v +client.capabilities.discover() + | + +--> client.capabilities.negotiate(remote, requirements) +``` + +Both synchronous local transports and asynchronous future remote transports are supported. + +## 9. Security and governance + +Capability negotiation answers only: + +> Can these two endpoints speak a compatible protocol and which capabilities do both claim? + +It does **not** answer: + +> Is this operation authorized? + +Authorization remains owned by Decisions, Waitpoints, workflow gates, leases, policy and operation lifecycle. + +```text +capability match != authorization +``` + +## 10. Compatibility with existing Host Capability Descriptor + +The existing host descriptor already models: + +- host identity; +- observability level; +- capability evidence source; +- friction observability; +- redaction posture. + +M-040 preserves it as an authoritative host-observability contract. + +Later milestones may adapt it to the generic protocol through an explicit bridge, but no direct replacement is required in MS-003. From e38ca43b76fe28dea0abba4ee894fdc609ca58a4 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:10:08 +0800 Subject: [PATCH 046/166] docs: index protocol negotiation architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index dfd5db4d..13f5a5bf 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -28,6 +28,7 @@ - [Agent Runtime Continuity](./agent-runtime-continuity.md) - [Runtime, Control and Integration Ownership](./runtime-control-ownership.md) - [Protocol, SDK and Progressive Monorepo](./protocol-sdk-monorepo.md) +- [Protocol Versioning and Capability Negotiation](./protocol-negotiation.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 6aca7e4a28529f2a554d38a1a6b8e7f0f061d87f Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:12:23 +0800 Subject: [PATCH 047/166] feat(m040): add runtime-port package --- packages/runtime-port/package.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/runtime-port/package.json diff --git a/packages/runtime-port/package.json b/packages/runtime-port/package.json new file mode 100644 index 00000000..f6740da0 --- /dev/null +++ b/packages/runtime-port/package.json @@ -0,0 +1,17 @@ +{ + "name": "@cortex-agent/runtime-port", + "version": "0.0.0", + "private": true, + "description": "RuntimePort contracts for Cortex Agent execution backends", + "main": "src/index.js", + "exports": { + ".": "./src/index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "license": "MIT", + "dependencies": { + "@cortex-agent/protocol": "workspace:*" + } +} From 031ffff4b0eff4a35af19f9a4584acf7ced54c99 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:12:26 +0800 Subject: [PATCH 048/166] feat(m040): define RuntimePort v1 contract --- packages/runtime-port/src/index.js | 104 +++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 packages/runtime-port/src/index.js diff --git a/packages/runtime-port/src/index.js b/packages/runtime-port/src/index.js new file mode 100644 index 00000000..74a0e0d2 --- /dev/null +++ b/packages/runtime-port/src/index.js @@ -0,0 +1,104 @@ +"use strict"; + +let protocol; +try { + protocol = require("@cortex-agent/protocol"); +} catch (_) { + protocol = require("../../protocol/src/index.js"); +} + +const RUNTIME_PORT_CAPABILITIES = Object.freeze([ + "runtime.discover", + "runtime.health", + "runtime.run.create", + "runtime.run.cancel", + "runtime.run.status", + "runtime.run.wait", + "runtime.run.send", + "runtime.timeline.read", + "runtime.run.archive", +]); + +const METHOD_CAPABILITY = Object.freeze({ + discoverRuntime: "runtime.discover", + health: "runtime.health", + createRun: "runtime.run.create", + send: "runtime.run.send", + cancel: "runtime.run.cancel", + getStatus: "runtime.run.status", + getTimeline: "runtime.timeline.read", + wait: "runtime.run.wait", + archive: "runtime.run.archive", +}); + +class RuntimePortError extends Error { + constructor(code, details = {}) { + super(`[runtime-port:${code}] ${JSON.stringify(details)}`); + this.name = "RuntimePortError"; + this.code = code; + this.details = details; + } +} + +function unsupported(capability, details = {}) { + throw new RuntimePortError("ERR_RUNTIME_CAPABILITY_UNSUPPORTED", { + capability, + ...details, + }); +} + +function normalizeRuntimeDescriptor(input) { + const descriptor = protocol.createCapabilityDescriptor(input); + if (descriptor.protocol !== protocol.RUNTIME_PROTOCOL_NAME) { + throw new RuntimePortError("ERR_RUNTIME_PROTOCOL_REQUIRED", { + protocol: descriptor.protocol, + }); + } + return descriptor; +} + +function createRuntimePort(options = {}) { + const descriptor = normalizeRuntimeDescriptor(options.descriptor || {}); + const operations = options.operations || {}; + const declared = new Set(descriptor.capabilities); + + const port = { + descriptor, + }; + + for (const [method, capability] of Object.entries(METHOD_CAPABILITY)) { + const implementation = operations[method]; + if (declared.has(capability) && typeof implementation !== "function") { + throw new RuntimePortError("ERR_RUNTIME_OPERATION_MISSING", { + method, + capability, + }); + } + port[method] = typeof implementation === "function" + ? implementation + : (...args) => unsupported(capability, { method, args_count: args.length }); + } + + return Object.freeze(port); +} + +function hasRuntimeCapability(port, capability) { + if (!port || !port.descriptor) return false; + return port.descriptor.capabilities.includes(capability); +} + +function assertRuntimeCapability(port, capability) { + if (!hasRuntimeCapability(port, capability)) { + unsupported(capability); + } + return true; +} + +module.exports = { + RUNTIME_PORT_CAPABILITIES, + METHOD_CAPABILITY, + RuntimePortError, + createRuntimePort, + hasRuntimeCapability, + assertRuntimeCapability, +}; From dba2252ff4f9d1301559f47f135e0fc097151046 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:12:28 +0800 Subject: [PATCH 049/166] feat(m040): bridge legacy adapters to RuntimePort --- lib/runtime-port/legacy-adapter-bridge.js | 165 ++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 lib/runtime-port/legacy-adapter-bridge.js diff --git a/lib/runtime-port/legacy-adapter-bridge.js b/lib/runtime-port/legacy-adapter-bridge.js new file mode 100644 index 00000000..1abcaf42 --- /dev/null +++ b/lib/runtime-port/legacy-adapter-bridge.js @@ -0,0 +1,165 @@ +"use strict"; + +const { createRef } = require("../../packages/protocol/src/index.js"); +const { + createRuntimePort, +} = require("../../packages/runtime-port/src/index.js"); + +const DEFAULT_WAIT_INTERVAL_MS = 50; +const DEFAULT_WAIT_TIMEOUT_MS = 30_000; + +function adapterRunId(refOrId) { + if (typeof refOrId !== "string" || !refOrId) { + const error = new Error("run reference or id is required"); + error.code = "ERR_RUN_REF_REQUIRED"; + throw error; + } + return refOrId.startsWith("run:") ? refOrId.slice("run:".length) : refOrId; +} + +function normalizeStatus(report) { + if (!report || typeof report !== "object") return "unknown"; + if (report.status === "not_found") return "pending"; + if (report.error) return "failed"; + if (report.result) return "completed"; + const value = String(report.status || "").toLowerCase(); + if (["ok", "success", "completed"].includes(value)) return "completed"; + if (["failed", "error"].includes(value)) return "failed"; + if (["cancelled", "canceled"].includes(value)) return "cancelled"; + if (["running", "pending", "queued", "waiting"].includes(value)) return value; + return value || "unknown"; +} + +function normalizeRunResult(result) { + const runId = result && (result.runId || result.run_id); + if (!runId) { + const error = new Error("legacy adapter invoke() returned no runId"); + error.code = "ERR_LEGACY_ADAPTER_RUN_ID_MISSING"; + throw error; + } + return { + run_ref: createRef("run", runId), + run_id: runId, + status: normalizeStatus(result), + result: result.result ?? null, + error: result.error ?? null, + latency_ms: result.latency_ms ?? null, + }; +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function createLegacyAdapterRuntimePort(adapter, options = {}) { + if (!adapter || typeof adapter.discover !== "function" + || typeof adapter.health !== "function" + || typeof adapter.invoke !== "function" + || typeof adapter.cancel !== "function" + || typeof adapter.report !== "function") { + const error = new Error("legacy adapter must implement discover/health/invoke/cancel/report"); + error.code = "ERR_LEGACY_ADAPTER_CONTRACT"; + throw error; + } + + const discovered = adapter.discover() || {}; + const adapterType = discovered.adapter_type || options.adapterType || "legacy"; + const implementationVersion = discovered.version || null; + + const descriptor = { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: `native-adapter:${adapterType}`, + implementation_version: implementationVersion, + capabilities: [ + "runtime.discover", + "runtime.health", + "runtime.run.create", + "runtime.run.cancel", + "runtime.run.status", + "runtime.run.wait", + ], + }; + + return createRuntimePort({ + descriptor, + operations: { + discoverRuntime() { + return { + descriptor, + adapter: discovered, + }; + }, + + health() { + return adapter.health(); + }, + + async createRun(payload, invokeOptions = {}) { + const result = await adapter.invoke(payload, invokeOptions); + return normalizeRunResult(result); + }, + + async cancel(refOrId, cancelOptions = {}) { + const runId = adapterRunId(refOrId); + const result = await adapter.cancel(runId, cancelOptions); + return { + run_ref: createRef("run", runId), + run_id: runId, + cancelled: Boolean(result && result.cancelled), + error: result && result.error ? result.error : null, + }; + }, + + async getStatus(refOrId, reportOptions = {}) { + const runId = adapterRunId(refOrId); + const report = await adapter.report(runId, reportOptions); + return { + run_ref: createRef("run", runId), + run_id: runId, + status: normalizeStatus(report), + report, + }; + }, + + async wait(refOrId, waitOptions = {}) { + const runId = adapterRunId(refOrId); + const timeoutMs = Number.isFinite(waitOptions.timeout_ms) + ? Math.max(0, waitOptions.timeout_ms) + : DEFAULT_WAIT_TIMEOUT_MS; + const intervalMs = Number.isFinite(waitOptions.interval_ms) + ? Math.max(1, waitOptions.interval_ms) + : DEFAULT_WAIT_INTERVAL_MS; + const reportOptions = waitOptions.report_options || {}; + const started = Date.now(); + + while (true) { + const report = await adapter.report(runId, reportOptions); + const status = normalizeStatus(report); + if (["completed", "failed", "cancelled"].includes(status)) { + return { + run_ref: createRef("run", runId), + run_id: runId, + status, + report, + }; + } + if (Date.now() - started >= timeoutMs) { + const error = new Error(`wait timed out for ${runId}`); + error.code = "ERR_RUNTIME_WAIT_TIMEOUT"; + error.run_id = runId; + throw error; + } + await delay(intervalMs); + } + }, + }, + }); +} + +module.exports = { + createLegacyAdapterRuntimePort, + adapterRunId, + normalizeStatus, + normalizeRunResult, +}; From 85dc4d0b41fcf0c0119d07e4a0432a9c6752ce72 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:12:31 +0800 Subject: [PATCH 050/166] test(m040): cover RuntimePort contract --- tests/runtime-port/runtime-port.test.js | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/runtime-port/runtime-port.test.js diff --git a/tests/runtime-port/runtime-port.test.js b/tests/runtime-port/runtime-port.test.js new file mode 100644 index 00000000..f060aef4 --- /dev/null +++ b/tests/runtime-port/runtime-port.test.js @@ -0,0 +1,59 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const runtime = require(path.join(ROOT, "packages", "runtime-port", "src")); + +function descriptor(capabilities) { + return { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: "test-runtime", + capabilities, + }; +} + +test("RuntimePort requires an implementation for every declared capability", () => { + assert.throws( + () => runtime.createRuntimePort({ + descriptor: descriptor(["runtime.health"]), + operations: {}, + }), + (error) => error.code === "ERR_RUNTIME_OPERATION_MISSING", + ); +}); + +test("RuntimePort exposes unsupported operations explicitly", () => { + const port = runtime.createRuntimePort({ + descriptor: descriptor(["runtime.health"]), + operations: { + health() { return { ready: true }; }, + }, + }); + + assert.deepEqual(port.health(), { ready: true }); + assert.equal(runtime.hasRuntimeCapability(port, "runtime.run.send"), false); + assert.throws( + () => port.send("run:x", { message: "hello" }), + (error) => error.code === "ERR_RUNTIME_CAPABILITY_UNSUPPORTED" + && error.details.capability === "runtime.run.send", + ); +}); + +test("RuntimePort rejects non-runtime protocol descriptors", () => { + assert.throws( + () => runtime.createRuntimePort({ + descriptor: { + protocol: "cortex-project", + protocol_version: "1.0", + implementation: "wrong", + capabilities: ["project.validation.run"], + }, + operations: {}, + }), + (error) => error.code === "ERR_RUNTIME_PROTOCOL_REQUIRED", + ); +}); From 47cc4154232daa2f2cf52204c1d3df55eb5ea725 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:12:33 +0800 Subject: [PATCH 051/166] test(m040): cover native adapter compatibility bridge --- .../legacy-adapter-bridge.test.js | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/runtime-port/legacy-adapter-bridge.test.js diff --git a/tests/runtime-port/legacy-adapter-bridge.test.js b/tests/runtime-port/legacy-adapter-bridge.test.js new file mode 100644 index 00000000..12d45810 --- /dev/null +++ b/tests/runtime-port/legacy-adapter-bridge.test.js @@ -0,0 +1,103 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + createLegacyAdapterRuntimePort, +} = require(path.join(ROOT, "lib", "runtime-port", "legacy-adapter-bridge.js")); + +function fakeAdapter() { + const calls = []; + let reportCount = 0; + return { + calls, + discover() { + return { + adapter_type: "fake", + version: "0.1.0", + capabilities: ["text_generation"], + }; + }, + async health() { + calls.push(["health"]); + return { status: "ok", ready: true }; + }, + async invoke(payload, options) { + calls.push(["invoke", payload, options]); + return { + runId: "R-FAKE-1", + status: "ok", + result: { answer: 42 }, + error: null, + latency_ms: 7, + }; + }, + async cancel(runId, options) { + calls.push(["cancel", runId, options]); + return { runId, cancelled: true, error: null }; + }, + async report(runId, options) { + calls.push(["report", runId, options]); + reportCount += 1; + if (reportCount < 2) { + return { runId, status: "not_found", result: null, error: null }; + } + return { runId, status: "ok", result: { answer: 42 }, error: null }; + }, + }; +} + +test("legacy adapter bridge declares only capabilities it can actually provide", () => { + const port = createLegacyAdapterRuntimePort(fakeAdapter()); + assert.equal(port.descriptor.implementation, "native-adapter:fake"); + assert.ok(port.descriptor.capabilities.includes("runtime.run.create")); + assert.ok(port.descriptor.capabilities.includes("runtime.run.wait")); + assert.equal(port.descriptor.capabilities.includes("runtime.run.send"), false); + assert.equal(port.descriptor.capabilities.includes("runtime.timeline.read"), false); + assert.equal(port.descriptor.capabilities.includes("runtime.run.archive"), false); +}); + +test("legacy invoke maps to createRun and preserves a canonical RunRef", async () => { + const adapter = fakeAdapter(); + const port = createLegacyAdapterRuntimePort(adapter); + const result = await port.createRun({ task: "test" }, { projectRoot: "/tmp/project" }); + assert.equal(result.run_ref, "run:R-FAKE-1"); + assert.equal(result.run_id, "R-FAKE-1"); + assert.equal(result.status, "completed"); + assert.deepEqual(result.result, { answer: 42 }); + assert.equal(adapter.calls[0][0], "invoke"); +}); + +test("legacy cancel and status delegate without inventing vendor state", async () => { + const adapter = fakeAdapter(); + const port = createLegacyAdapterRuntimePort(adapter); + const cancel = await port.cancel("run:R-FAKE-1", { reason: "test" }); + assert.equal(cancel.cancelled, true); + assert.equal(cancel.run_ref, "run:R-FAKE-1"); + + const status = await port.getStatus("run:R-FAKE-1"); + assert.equal(status.status, "pending"); + assert.equal(status.run_ref, "run:R-FAKE-1"); +}); + +test("legacy wait polls report until terminal state", async () => { + const adapter = fakeAdapter(); + const port = createLegacyAdapterRuntimePort(adapter); + const result = await port.wait("run:R-FAKE-1", { timeout_ms: 100, interval_ms: 1 }); + assert.equal(result.status, "completed"); + assert.equal(result.report.result.answer, 42); +}); + +test("legacy bridge fails closed for unsupported send/timeline/archive", () => { + const port = createLegacyAdapterRuntimePort(fakeAdapter()); + for (const call of [ + () => port.send("run:R-1", { message: "x" }), + () => port.getTimeline("run:R-1"), + () => port.archive("run:R-1"), + ]) { + assert.throws(call, (error) => error.code === "ERR_RUNTIME_CAPABILITY_UNSUPPORTED"); + } +}); From 6ed87edb64b6558e5c71852b8e1c2343ad203d42 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:13:32 +0800 Subject: [PATCH 052/166] refactor(m040): keep legacy runtime ids behind RunRef --- lib/runtime-port/legacy-adapter-bridge.js | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/runtime-port/legacy-adapter-bridge.js b/lib/runtime-port/legacy-adapter-bridge.js index 1abcaf42..83baa672 100644 --- a/lib/runtime-port/legacy-adapter-bridge.js +++ b/lib/runtime-port/legacy-adapter-bridge.js @@ -39,7 +39,6 @@ function normalizeRunResult(result) { } return { run_ref: createRef("run", runId), - run_id: runId, status: normalizeStatus(result), result: result.result ?? null, error: result.error ?? null, @@ -105,7 +104,6 @@ function createLegacyAdapterRuntimePort(adapter, options = {}) { const result = await adapter.cancel(runId, cancelOptions); return { run_ref: createRef("run", runId), - run_id: runId, cancelled: Boolean(result && result.cancelled), error: result && result.error ? result.error : null, }; @@ -116,9 +114,16 @@ function createLegacyAdapterRuntimePort(adapter, options = {}) { const report = await adapter.report(runId, reportOptions); return { run_ref: createRef("run", runId), - run_id: runId, status: normalizeStatus(report), - report, + result: report && report.result ? report.result : null, + error: report && report.error ? report.error : null, + evidence: { + result_present: Boolean(report && report.result), + error_present: Boolean(report && report.error), + rollback_present: Boolean(report && report.rollback), + rollback_failed: Boolean(report && report.rollback_failed), + written_at: report && report.written_at ? report.written_at : null, + }, }; }, @@ -139,9 +144,16 @@ function createLegacyAdapterRuntimePort(adapter, options = {}) { if (["completed", "failed", "cancelled"].includes(status)) { return { run_ref: createRef("run", runId), - run_id: runId, status, - report, + result: report && report.result ? report.result : null, + error: report && report.error ? report.error : null, + evidence: { + result_present: Boolean(report && report.result), + error_present: Boolean(report && report.error), + rollback_present: Boolean(report && report.rollback), + rollback_failed: Boolean(report && report.rollback_failed), + written_at: report && report.written_at ? report.written_at : null, + }, }; } if (Date.now() - started >= timeoutMs) { From c863a934b771d4b91b4f50775c7858a9b7349758 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:13:35 +0800 Subject: [PATCH 053/166] test(m040): guard opaque RunRef bridge semantics --- tests/runtime-port/legacy-adapter-bridge.test.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/runtime-port/legacy-adapter-bridge.test.js b/tests/runtime-port/legacy-adapter-bridge.test.js index 12d45810..79922de2 100644 --- a/tests/runtime-port/legacy-adapter-bridge.test.js +++ b/tests/runtime-port/legacy-adapter-bridge.test.js @@ -65,7 +65,6 @@ test("legacy invoke maps to createRun and preserves a canonical RunRef", async ( const port = createLegacyAdapterRuntimePort(adapter); const result = await port.createRun({ task: "test" }, { projectRoot: "/tmp/project" }); assert.equal(result.run_ref, "run:R-FAKE-1"); - assert.equal(result.run_id, "R-FAKE-1"); assert.equal(result.status, "completed"); assert.deepEqual(result.result, { answer: 42 }); assert.equal(adapter.calls[0][0], "invoke"); @@ -88,7 +87,8 @@ test("legacy wait polls report until terminal state", async () => { const port = createLegacyAdapterRuntimePort(adapter); const result = await port.wait("run:R-FAKE-1", { timeout_ms: 100, interval_ms: 1 }); assert.equal(result.status, "completed"); - assert.equal(result.report.result.answer, 42); + assert.equal(result.result.answer, 42); + assert.equal(result.evidence.result_present, true); }); test("legacy bridge fails closed for unsupported send/timeline/archive", () => { @@ -101,3 +101,12 @@ test("legacy bridge fails closed for unsupported send/timeline/archive", () => { assert.throws(call, (error) => error.code === "ERR_RUNTIME_CAPABILITY_UNSUPPORTED"); } }); + +test("legacy bridge does not expose a second raw run id beside canonical run_ref", async () => { + const port = createLegacyAdapterRuntimePort(fakeAdapter()); + const created = await port.createRun({ task: "test" }); + assert.equal(Object.prototype.hasOwnProperty.call(created, "run_id"), false); + const status = await port.getStatus(created.run_ref); + assert.equal(Object.prototype.hasOwnProperty.call(status, "run_id"), false); + assert.equal(Object.prototype.hasOwnProperty.call(status, "report"), false); +}); From e66b67365f805f43ea75e46ffcd03728855fbf12 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:13:37 +0800 Subject: [PATCH 054/166] chore(m040): register runtime-port workspace importer --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9780dcb1..ebc132fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,3 +15,9 @@ importers: '@cortex-agent/protocol': specifier: workspace:* version: link:../protocol + + packages/runtime-port: + dependencies: + '@cortex-agent/protocol': + specifier: workspace:* + version: link:../protocol From 59a2cdfe18310356bca8ef1f6bdf940c29cc76e1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:13:41 +0800 Subject: [PATCH 055/166] test(m040): include RuntimePort compatibility suites --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d60d695..c124854b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/management/management-query-cli.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js" }, "keywords": [ "ai", From bf6662eea3db2881d58f6bfbb6c2063900dd0b2c Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:13:43 +0800 Subject: [PATCH 056/166] ci(m040): validate RuntimePort changes --- .github/workflows/m040-validation.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml index 8d00dc98..188175be 100644 --- a/.github/workflows/m040-validation.yml +++ b/.github/workflows/m040-validation.yml @@ -10,10 +10,12 @@ on: - "pnpm-workspace.yaml" - "packages/**" - "lib/sdk/**" + - "lib/runtime-port/**" - "lib/commands/management/query.js" - "tests/architecture/**" - "tests/protocol/**" - "tests/sdk/**" + - "tests/runtime-port/**" - "tests/management/management-query-cli.test.js" - ".github/workflows/m040-validation.yml" pull_request: From 2c519579868f7e301f13a54d3799e2512165da44 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:13:46 +0800 Subject: [PATCH 057/166] test(m040): guard RuntimePort package boundary --- tests/architecture/m040-monorepo-boundary.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/architecture/m040-monorepo-boundary.test.js b/tests/architecture/m040-monorepo-boundary.test.js index 7869d33d..8ce25e01 100644 --- a/tests/architecture/m040-monorepo-boundary.test.js +++ b/tests/architecture/m040-monorepo-boundary.test.js @@ -69,3 +69,18 @@ test("ProjectRef identity is derived from topology before directory basename fal assert.match(text, /path\.basename\(project\.root\)/); assert.ok(text.indexOf("current.self.project_id") < text.indexOf("path.basename(project.root)")); }); + +test("@cortex-agent/runtime-port depends only on portable protocol contracts", () => { + const pkg = JSON.parse(fs.readFileSync( + path.join(ROOT, "packages", "runtime-port", "package.json"), + "utf8", + )); + assert.deepEqual(Object.keys(pkg.dependencies || {}), ["@cortex-agent/protocol"]); + const text = fs.readFileSync( + path.join(ROOT, "packages", "runtime-port", "src", "index.js"), + "utf8", + ); + assert.equal(text.includes("../../lib"), false); + assert.equal(text.includes("node:fs"), false); + assert.equal(text.includes("child_process"), false); +}); From ec13334b8541dd8e631127e145e46eb51c59f494 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:14:53 +0800 Subject: [PATCH 058/166] docs: define M-040 RuntimePort v1 --- docs/architecture/runtime-port-v1.md | 162 +++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 docs/architecture/runtime-port-v1.md diff --git a/docs/architecture/runtime-port-v1.md b/docs/architecture/runtime-port-v1.md new file mode 100644 index 00000000..072aab98 --- /dev/null +++ b/docs/architecture/runtime-port-v1.md @@ -0,0 +1,162 @@ +# RuntimePort v1 and Native Adapter Compatibility + +> **Status**: M-040 MS-004 baseline +> **Validated**: 2026-09-29 +> **CI**: M-040 Architecture Validation — 43/43 PASS + +## 1. Purpose + +RuntimePort is the runtime-agnostic execution contract between Cortex governance/control logic and concrete execution backends. + +It does not replace the existing native Adapter contract. Existing adapters are preserved through an explicit compatibility bridge. + +## 2. RuntimePort surface + +```text +discoverRuntime() +health() +createRun() +send() +cancel() +getStatus() +getTimeline() +wait() +archive() +``` + +Each operation maps to an explicit capability. + +Unsupported operations are still present on the port but fail with: + +```text +ERR_RUNTIME_CAPABILITY_UNSUPPORTED +``` + +This prevents callers from inferring support from method existence alone. + +## 3. Runtime capabilities + +Current v1 capability vocabulary: + +```text +runtime.discover +runtime.health +runtime.run.create +runtime.run.cancel +runtime.run.status +runtime.run.wait +runtime.run.send +runtime.timeline.read +runtime.run.archive +``` + +A backend declares only capabilities it actually implements. + +## 4. Existing native Adapter contract + +The existing 5-method contract remains valid: + +```text +discover() +health() +invoke() +cancel() +report() +``` + +It continues to own its existing dispatch journal/evidence behavior. + +## 5. Compatibility bridge + +`lib/runtime-port/legacy-adapter-bridge.js` adapts the legacy contract to RuntimePort. + +Mapping: + +| Legacy Adapter | RuntimePort | +| :--- | :--- | +| discover | discoverRuntime | +| health | health | +| invoke | createRun | +| cancel | cancel | +| report | getStatus | +| report polling | wait | +| — | send unsupported | +| — | getTimeline unsupported | +| — | archive unsupported | + +The bridge does not claim unsupported capabilities. + +## 6. Run identity + +RuntimePort exposes a canonical opaque `RunRef`: + +```text +run:R-... +``` + +The compatibility bridge does not expose a second public raw `run_id` field beside the canonical ref. + +The raw backend run identifier remains an implementation detail encoded behind the opaque reference boundary. + +## 7. Status normalization + +The bridge normalizes legacy terminal results into runtime lifecycle status. + +Examples: + +```text +result present -> completed +error present -> failed +report not found -> pending +cancel/cancelled -> cancelled +``` + +This status is execution evidence only. + +It does not directly complete a Cortex Mission or Coordination Task. + +## 8. Wait semantics + +Legacy adapters do not expose an independent run handle before `invoke()` returns, but the adapter journal can still be queried for an existing run. + +The compatibility bridge implements `wait()` by bounded polling of `report()`. + +Timeout fails explicitly with: + +```text +ERR_RUNTIME_WAIT_TIMEOUT +``` + +Future runtime backends such as Paseo may implement native wait/stream semantics directly. + +## 9. Evidence boundary + +Legacy journal ownership remains unchanged under the adapter runtime dispatch directory. + +RuntimePort status projection exposes only normalized result/error/evidence summary. + +It does not make runtime-specific journal layout part of the portable RuntimePort package. + +## 10. Package boundary + +`@cortex-agent/runtime-port` depends only on `@cortex-agent/protocol`. + +It does not depend on: + +- filesystem; +- child_process; +- native adapters; +- Paseo; +- Management API; +- Coordination; +- UI. + +Concrete implementations live outside the portable package. + +## 11. Governance invariants + +- RuntimePort does not own Mission truth. +- Runtime completion does not imply governance completion. +- Runtime capability availability does not imply authorization. +- Native Adapter compatibility remains available without Paseo. +- Provider-specific metadata stays below the RuntimePort boundary. From e762d7f030b2d3edb0f031bb854e7d4c692e31e4 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:14:55 +0800 Subject: [PATCH 059/166] docs: index RuntimePort v1 architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 13f5a5bf..525ec25f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -29,6 +29,7 @@ - [Runtime, Control and Integration Ownership](./runtime-control-ownership.md) - [Protocol, SDK and Progressive Monorepo](./protocol-sdk-monorepo.md) - [Protocol Versioning and Capability Negotiation](./protocol-negotiation.md) +- [RuntimePort v1 and Native Adapter Compatibility](./runtime-port-v1.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 6120ada9edcd43184c62eb839d723853c99b77ae Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:18:34 +0800 Subject: [PATCH 060/166] fix(m040): enforce declared RuntimePort capabilities --- packages/runtime-port/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-port/src/index.js b/packages/runtime-port/src/index.js index 74a0e0d2..5c6fc7a4 100644 --- a/packages/runtime-port/src/index.js +++ b/packages/runtime-port/src/index.js @@ -74,7 +74,7 @@ function createRuntimePort(options = {}) { capability, }); } - port[method] = typeof implementation === "function" + port[method] = declared.has(capability) ? implementation : (...args) => unsupported(capability, { method, args_count: args.length }); } From c89721499fd0196dd94d51229d189a8592f5f253 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:18:36 +0800 Subject: [PATCH 061/166] fix(m040): honor legacy adapter invoke cancel support --- lib/runtime-port/legacy-adapter-bridge.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/runtime-port/legacy-adapter-bridge.js b/lib/runtime-port/legacy-adapter-bridge.js index 83baa672..a431bc2f 100644 --- a/lib/runtime-port/legacy-adapter-bridge.js +++ b/lib/runtime-port/legacy-adapter-bridge.js @@ -65,19 +65,21 @@ function createLegacyAdapterRuntimePort(adapter, options = {}) { const adapterType = discovered.adapter_type || options.adapterType || "legacy"; const implementationVersion = discovered.version || null; + const capabilities = [ + "runtime.discover", + "runtime.health", + "runtime.run.status", + "runtime.run.wait", + ]; + if (discovered.invoke_supported !== false) capabilities.push("runtime.run.create"); + if (discovered.cancel_supported !== false) capabilities.push("runtime.run.cancel"); + const descriptor = { protocol: "cortex-runtime", protocol_version: "1.0", implementation: `native-adapter:${adapterType}`, implementation_version: implementationVersion, - capabilities: [ - "runtime.discover", - "runtime.health", - "runtime.run.create", - "runtime.run.cancel", - "runtime.run.status", - "runtime.run.wait", - ], + capabilities, }; return createRuntimePort({ From 722f59b2c2177ad9ed58cea400d798337235384b Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:18:39 +0800 Subject: [PATCH 062/166] test(m040): prevent undeclared RuntimePort operations --- tests/runtime-port/runtime-port.test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/runtime-port/runtime-port.test.js b/tests/runtime-port/runtime-port.test.js index f060aef4..1feba7b2 100644 --- a/tests/runtime-port/runtime-port.test.js +++ b/tests/runtime-port/runtime-port.test.js @@ -57,3 +57,20 @@ test("RuntimePort rejects non-runtime protocol descriptors", () => { (error) => error.code === "ERR_RUNTIME_PROTOCOL_REQUIRED", ); }); + +test("RuntimePort never exposes an undeclared capability even when an operation function is supplied", () => { + let invoked = false; + const port = runtime.createRuntimePort({ + descriptor: descriptor(["runtime.health"]), + operations: { + health() { return { ready: true }; }, + createRun() { invoked = true; return { run_ref: "run:unexpected" }; }, + }, + }); + + assert.throws( + () => port.createRun({ task: "must-not-run" }), + (error) => error.code === "ERR_RUNTIME_CAPABILITY_UNSUPPORTED", + ); + assert.equal(invoked, false); +}); From c25c42d3e6aed80f9ab6f2fc34292aa0c16c72e3 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:18:42 +0800 Subject: [PATCH 063/166] test(m040): keep observer adapters non-executable --- .../legacy-adapter-bridge.test.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/runtime-port/legacy-adapter-bridge.test.js b/tests/runtime-port/legacy-adapter-bridge.test.js index 79922de2..33b574eb 100644 --- a/tests/runtime-port/legacy-adapter-bridge.test.js +++ b/tests/runtime-port/legacy-adapter-bridge.test.js @@ -110,3 +110,26 @@ test("legacy bridge does not expose a second raw run id beside canonical run_ref assert.equal(Object.prototype.hasOwnProperty.call(status, "run_id"), false); assert.equal(Object.prototype.hasOwnProperty.call(status, "report"), false); }); + +test("observer-only legacy adapters do not gain invoke/cancel capabilities from method presence", () => { + const adapter = fakeAdapter(); + const originalDiscover = adapter.discover; + adapter.discover = () => ({ + ...originalDiscover(), + observer: true, + invoke_supported: false, + cancel_supported: false, + }); + + const port = createLegacyAdapterRuntimePort(adapter); + assert.equal(port.descriptor.capabilities.includes("runtime.run.create"), false); + assert.equal(port.descriptor.capabilities.includes("runtime.run.cancel"), false); + assert.throws( + () => port.createRun({ task: "forbidden" }), + (error) => error.code === "ERR_RUNTIME_CAPABILITY_UNSUPPORTED", + ); + assert.throws( + () => port.cancel("run:R-1"), + (error) => error.code === "ERR_RUNTIME_CAPABILITY_UNSUPPORTED", + ); +}); From 04e75dadc8a1a2b02652686e70aef21273cd78ba Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:19:50 +0800 Subject: [PATCH 064/166] feat(m040): add RuntimeEndpoint opaque ref --- packages/protocol/src/refs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/src/refs.js b/packages/protocol/src/refs.js index cb296a41..e463d089 100644 --- a/packages/protocol/src/refs.js +++ b/packages/protocol/src/refs.js @@ -7,6 +7,7 @@ const REF_KINDS = Object.freeze([ "agent", "host", "runtime", + "runtime-endpoint", "session", ]); From 8ca3bc3b0706d952020382c32add1d52cb8e5070 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:19:53 +0800 Subject: [PATCH 065/166] feat(m040): define RuntimeEndpoint topology contract --- packages/runtime-port/src/topology.js | 317 ++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 packages/runtime-port/src/topology.js diff --git a/packages/runtime-port/src/topology.js b/packages/runtime-port/src/topology.js new file mode 100644 index 00000000..af931a75 --- /dev/null +++ b/packages/runtime-port/src/topology.js @@ -0,0 +1,317 @@ +"use strict"; + +let protocol; +try { + protocol = require("@cortex-agent/protocol"); +} catch (_) { + protocol = require("../../protocol/src/index.js"); +} + +const RUNTIME_ENDPOINT_SCHEMA_VERSION = "1"; +const RUNTIME_REQUIREMENT_SCHEMA_VERSION = "1"; + +const RUNTIME_LOCATIONS = Object.freeze(["local", "remote"]); +const RUNTIME_TRANSPORTS = Object.freeze([ + "in-process", + "stdio", + "http", + "websocket", + "ipc", + "ssh", + "custom", +]); +const RUNTIME_AVAILABILITY = Object.freeze([ + "available", + "unavailable", + "unknown", +]); + +const ENDPOINT_KEYS = new Set([ + "schema_version", + "endpoint_ref", + "host_ref", + "runtime_ref", + "location", + "transport", + "availability", + "descriptor", + "workspace_refs", +]); + +const REQUIREMENT_KEYS = new Set([ + "schema_version", + "requirement_id", + "required_capabilities", + "endpoint_ref", + "host_ref", + "runtime_ref", + "location", + "transport", + "workspace_ref", + "require_available", +]); + +class RuntimeTopologyError extends Error { + constructor(code, details = {}) { + super(`[runtime-topology:${code}] ${JSON.stringify(details)}`); + this.name = "RuntimeTopologyError"; + this.code = code; + this.details = details; + } +} + +function rejectUnknownKeys(value, known, where) { + for (const key of Object.keys(value || {})) { + if (!known.has(key)) { + throw new RuntimeTopologyError("ERR_RUNTIME_TOPOLOGY_FIELD_UNKNOWN", { + where, + key, + }); + } + } +} + +function requireRef(value, kind, where) { + if (!protocol.isRef(value, kind)) { + throw new RuntimeTopologyError("ERR_RUNTIME_TOPOLOGY_REF_INVALID", { + where, + expected_kind: kind, + value, + }); + } + return value; +} + +function optionalRef(value, kind, where) { + if (value == null) return null; + return requireRef(value, kind, where); +} + +function requireEnum(value, allowed, where) { + if (!allowed.includes(value)) { + throw new RuntimeTopologyError("ERR_RUNTIME_TOPOLOGY_ENUM_INVALID", { + where, + value, + allowed, + }); + } + return value; +} + +function normalizeRuntimeCapabilities(values, where) { + const capabilities = protocol.validateCapabilityList(values || []); + for (const capability of capabilities) { + const parsed = protocol.parseCapabilityId(capability); + if (!parsed || parsed.namespace !== "runtime") { + throw new RuntimeTopologyError("ERR_RUNTIME_CAPABILITY_NAMESPACE", { + where, + capability, + }); + } + } + return capabilities; +} + +function normalizeRuntimeEndpoint(input) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new RuntimeTopologyError("ERR_RUNTIME_ENDPOINT_INVALID", {}); + } + rejectUnknownKeys(input, ENDPOINT_KEYS, "endpoint"); + + const schemaVersion = input.schema_version == null + ? RUNTIME_ENDPOINT_SCHEMA_VERSION + : String(input.schema_version); + if (schemaVersion !== RUNTIME_ENDPOINT_SCHEMA_VERSION) { + throw new RuntimeTopologyError("ERR_RUNTIME_ENDPOINT_SCHEMA_VERSION", { + expected: RUNTIME_ENDPOINT_SCHEMA_VERSION, + received: schemaVersion, + }); + } + + const descriptor = protocol.createCapabilityDescriptor(input.descriptor || {}); + if (descriptor.protocol !== protocol.RUNTIME_PROTOCOL_NAME) { + throw new RuntimeTopologyError("ERR_RUNTIME_ENDPOINT_PROTOCOL", { + protocol: descriptor.protocol, + }); + } + normalizeRuntimeCapabilities(descriptor.capabilities, "endpoint.descriptor.capabilities"); + + const workspaceRefs = Array.isArray(input.workspace_refs) + ? input.workspace_refs.map((value, index) => + requireRef(value, "workspace", `endpoint.workspace_refs[${index}]`)) + : []; + const dedupedWorkspaces = Object.freeze([...new Set(workspaceRefs)]); + + return Object.freeze({ + schema_version: RUNTIME_ENDPOINT_SCHEMA_VERSION, + endpoint_ref: requireRef(input.endpoint_ref, "runtime-endpoint", "endpoint.endpoint_ref"), + host_ref: requireRef(input.host_ref, "host", "endpoint.host_ref"), + runtime_ref: requireRef(input.runtime_ref, "runtime", "endpoint.runtime_ref"), + location: requireEnum(input.location, RUNTIME_LOCATIONS, "endpoint.location"), + transport: requireEnum(input.transport, RUNTIME_TRANSPORTS, "endpoint.transport"), + availability: requireEnum( + input.availability == null ? "unknown" : input.availability, + RUNTIME_AVAILABILITY, + "endpoint.availability", + ), + descriptor, + workspace_refs: dedupedWorkspaces, + }); +} + +function normalizeRuntimeRequirement(input) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new RuntimeTopologyError("ERR_RUNTIME_REQUIREMENT_INVALID", {}); + } + rejectUnknownKeys(input, REQUIREMENT_KEYS, "requirement"); + + const schemaVersion = input.schema_version == null + ? RUNTIME_REQUIREMENT_SCHEMA_VERSION + : String(input.schema_version); + if (schemaVersion !== RUNTIME_REQUIREMENT_SCHEMA_VERSION) { + throw new RuntimeTopologyError("ERR_RUNTIME_REQUIREMENT_SCHEMA_VERSION", { + expected: RUNTIME_REQUIREMENT_SCHEMA_VERSION, + received: schemaVersion, + }); + } + + if (typeof input.requirement_id !== "string" || !input.requirement_id.trim()) { + throw new RuntimeTopologyError("ERR_RUNTIME_REQUIREMENT_ID", {}); + } + + return Object.freeze({ + schema_version: RUNTIME_REQUIREMENT_SCHEMA_VERSION, + requirement_id: input.requirement_id.trim(), + required_capabilities: normalizeRuntimeCapabilities( + input.required_capabilities || [], + "requirement.required_capabilities", + ), + endpoint_ref: optionalRef( + input.endpoint_ref, + "runtime-endpoint", + "requirement.endpoint_ref", + ), + host_ref: optionalRef(input.host_ref, "host", "requirement.host_ref"), + runtime_ref: optionalRef(input.runtime_ref, "runtime", "requirement.runtime_ref"), + location: input.location == null + ? null + : requireEnum(input.location, RUNTIME_LOCATIONS, "requirement.location"), + transport: input.transport == null + ? null + : requireEnum(input.transport, RUNTIME_TRANSPORTS, "requirement.transport"), + workspace_ref: optionalRef( + input.workspace_ref, + "workspace", + "requirement.workspace_ref", + ), + require_available: input.require_available !== false, + }); +} + +function reject(reasons, code, details = {}) { + reasons.push(Object.freeze({ code, details: Object.freeze({ ...details }) })); +} + +function evaluateRuntimeEndpoint(requirement, endpoint) { + const reasons = []; + + if (requirement.endpoint_ref && endpoint.endpoint_ref !== requirement.endpoint_ref) { + reject(reasons, "endpoint_mismatch", { + expected: requirement.endpoint_ref, + actual: endpoint.endpoint_ref, + }); + } + if (requirement.host_ref && endpoint.host_ref !== requirement.host_ref) { + reject(reasons, "host_mismatch", { + expected: requirement.host_ref, + actual: endpoint.host_ref, + }); + } + if (requirement.runtime_ref && endpoint.runtime_ref !== requirement.runtime_ref) { + reject(reasons, "runtime_mismatch", { + expected: requirement.runtime_ref, + actual: endpoint.runtime_ref, + }); + } + if (requirement.location && endpoint.location !== requirement.location) { + reject(reasons, "location_mismatch", { + expected: requirement.location, + actual: endpoint.location, + }); + } + if (requirement.transport && endpoint.transport !== requirement.transport) { + reject(reasons, "transport_mismatch", { + expected: requirement.transport, + actual: endpoint.transport, + }); + } + if (requirement.require_available && endpoint.availability !== "available") { + reject(reasons, "runtime_unavailable", { + availability: endpoint.availability, + }); + } + if (requirement.workspace_ref + && !endpoint.workspace_refs.includes(requirement.workspace_ref)) { + reject(reasons, "workspace_unavailable", { + workspace_ref: requirement.workspace_ref, + }); + } + + const provided = new Set(endpoint.descriptor.capabilities); + for (const capability of requirement.required_capabilities) { + if (!provided.has(capability)) { + reject(reasons, "missing_runtime_capability", { capability }); + } + } + + return Object.freeze({ + endpoint_ref: endpoint.endpoint_ref, + accepted: reasons.length === 0, + reasons: Object.freeze(reasons), + }); +} + +function filterRuntimeEndpoints(requirementInput, endpointInputs) { + const requirement = normalizeRuntimeRequirement(requirementInput); + if (!Array.isArray(endpointInputs)) { + throw new RuntimeTopologyError("ERR_RUNTIME_ENDPOINT_LIST_INVALID", {}); + } + const endpoints = endpointInputs.map(normalizeRuntimeEndpoint); + const evaluations = endpoints + .map((endpoint) => ({ + endpoint, + evaluation: evaluateRuntimeEndpoint(requirement, endpoint), + })) + .sort((left, right) => + left.endpoint.endpoint_ref.localeCompare(right.endpoint.endpoint_ref)); + + return Object.freeze({ + requirement, + accepted: Object.freeze( + evaluations + .filter((item) => item.evaluation.accepted) + .map((item) => item.endpoint), + ), + rejected: Object.freeze( + evaluations + .filter((item) => !item.evaluation.accepted) + .map((item) => Object.freeze({ + endpoint: item.endpoint, + reasons: item.evaluation.reasons, + })), + ), + }); +} + +module.exports = { + RUNTIME_ENDPOINT_SCHEMA_VERSION, + RUNTIME_REQUIREMENT_SCHEMA_VERSION, + RUNTIME_LOCATIONS, + RUNTIME_TRANSPORTS, + RUNTIME_AVAILABILITY, + RuntimeTopologyError, + normalizeRuntimeEndpoint, + normalizeRuntimeRequirement, + evaluateRuntimeEndpoint, + filterRuntimeEndpoints, +}; From b35b36eea584e03c40d1ee29f69f8131e8eec8b1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:19:55 +0800 Subject: [PATCH 066/166] feat(m040): export runtime topology contracts --- packages/runtime-port/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime-port/src/index.js b/packages/runtime-port/src/index.js index 5c6fc7a4..6dad6e75 100644 --- a/packages/runtime-port/src/index.js +++ b/packages/runtime-port/src/index.js @@ -95,6 +95,7 @@ function assertRuntimeCapability(port, capability) { } module.exports = { + ...require("./topology"), RUNTIME_PORT_CAPABILITIES, METHOD_CAPABILITY, RuntimePortError, From 4ecac72e4ddec08e989cb55910fec933fb5efcb9 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:19:58 +0800 Subject: [PATCH 067/166] test(m040): cover RuntimeEndpoint hard filtering --- tests/runtime-port/topology.test.js | 115 ++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/runtime-port/topology.test.js diff --git a/tests/runtime-port/topology.test.js b/tests/runtime-port/topology.test.js new file mode 100644 index 00000000..1bc4b6a0 --- /dev/null +++ b/tests/runtime-port/topology.test.js @@ -0,0 +1,115 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const runtime = require(path.join(ROOT, "packages", "runtime-port", "src")); + +function endpoint(overrides = {}) { + return { + endpoint_ref: "runtime-endpoint:local:native:codex", + host_ref: "host:local", + runtime_ref: "runtime:native:codex", + location: "local", + transport: "stdio", + availability: "available", + descriptor: { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: "native-adapter:codex", + capabilities: [ + "runtime.discover", + "runtime.health", + "runtime.run.create", + "runtime.run.status", + ], + }, + workspace_refs: ["workspace:WS-1"], + ...overrides, + }; +} + +test("RuntimeEndpoint keeps machine HostRef separate from RuntimeRef", () => { + const value = runtime.normalizeRuntimeEndpoint(endpoint()); + assert.equal(value.host_ref, "host:local"); + assert.equal(value.runtime_ref, "runtime:native:codex"); + assert.equal(value.endpoint_ref, "runtime-endpoint:local:native:codex"); +}); + +test("RuntimeEndpoint accepts only runtime capability namespace", () => { + assert.throws( + () => runtime.normalizeRuntimeEndpoint(endpoint({ + descriptor: { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: "bad", + capabilities: ["project.validation.run"], + }, + })), + (error) => error.code === "ERR_RUNTIME_CAPABILITY_NAMESPACE", + ); +}); + +test("RuntimeRequirement hard-filters capabilities, availability and workspace", () => { + const result = runtime.filterRuntimeEndpoints({ + requirement_id: "REQ-1", + required_capabilities: ["runtime.run.create"], + workspace_ref: "workspace:WS-1", + }, [ + endpoint(), + endpoint({ + endpoint_ref: "runtime-endpoint:remote:paseo", + host_ref: "host:linux-server", + runtime_ref: "runtime:paseo:linux-server", + location: "remote", + transport: "websocket", + availability: "unknown", + workspace_refs: [], + }), + ]); + + assert.deepEqual( + result.accepted.map((item) => item.endpoint_ref), + ["runtime-endpoint:local:native:codex"], + ); + assert.equal(result.rejected.length, 1); + assert.ok(result.rejected[0].reasons.some((reason) => reason.code === "runtime_unavailable")); + assert.ok(result.rejected[0].reasons.some((reason) => reason.code === "workspace_unavailable")); +}); + +test("explicit endpoint selection never silently falls back", () => { + const result = runtime.filterRuntimeEndpoints({ + requirement_id: "REQ-explicit", + endpoint_ref: "runtime-endpoint:remote:paseo", + required_capabilities: ["runtime.run.create"], + require_available: false, + }, [ + endpoint(), + endpoint({ + endpoint_ref: "runtime-endpoint:remote:paseo", + host_ref: "host:linux-server", + runtime_ref: "runtime:paseo:linux-server", + location: "remote", + transport: "websocket", + availability: "unknown", + }), + ]); + + assert.deepEqual( + result.accepted.map((item) => item.endpoint_ref), + ["runtime-endpoint:remote:paseo"], + ); + const local = result.rejected.find( + (item) => item.endpoint.endpoint_ref === "runtime-endpoint:local:native:codex", + ); + assert.ok(local.reasons.some((reason) => reason.code === "endpoint_mismatch")); +}); + +test("unknown RuntimeEndpoint fields fail closed", () => { + assert.throws( + () => runtime.normalizeRuntimeEndpoint(endpoint({ vendor_magic: true })), + (error) => error.code === "ERR_RUNTIME_TOPOLOGY_FIELD_UNKNOWN", + ); +}); From 6ef921019bd792191cbc490a48b9fe8efc393b58 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:20:47 +0800 Subject: [PATCH 068/166] feat(m040): discover native RuntimeEndpoints --- lib/runtime-port/native-endpoint.js | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 lib/runtime-port/native-endpoint.js diff --git a/lib/runtime-port/native-endpoint.js b/lib/runtime-port/native-endpoint.js new file mode 100644 index 00000000..bc4f80d2 --- /dev/null +++ b/lib/runtime-port/native-endpoint.js @@ -0,0 +1,84 @@ +"use strict"; + +const { + createRef, + parseRef, +} = require("../../packages/protocol/src/index.js"); +const { + normalizeRuntimeEndpoint, +} = require("../../packages/runtime-port/src/index.js"); +const { + createLegacyAdapterRuntimePort, +} = require("./legacy-adapter-bridge.js"); + +function mapLegacyTransport(value) { + const transport = String(value || "").toLowerCase(); + if (transport.includes("websocket") || transport === "ws" || transport === "wss") { + return "websocket"; + } + if (transport.includes("http")) return "http"; + if (transport.includes("stdio")) return "stdio"; + if (transport.includes("ipc")) return "ipc"; + if (transport.includes("ssh")) return "ssh"; + if (transport.includes("in-process") || transport.includes("internal")) { + return "in-process"; + } + return "custom"; +} + +function endpointValue(hostRef, adapterType) { + const host = parseRef(hostRef, "host"); + if (!host) { + const error = new Error("native endpoint discovery requires a valid HostRef"); + error.code = "ERR_NATIVE_HOST_REF_INVALID"; + throw error; + } + return `${host.value}:native:${adapterType}`; +} + +async function discoverNativeRuntimeEndpoint(adapter, options = {}) { + const port = createLegacyAdapterRuntimePort(adapter, options); + const discovery = port.discoverRuntime(); + const adapterMeta = discovery.adapter || {}; + const adapterType = adapterMeta.adapter_type + || options.adapterType + || port.descriptor.implementation.replace(/^native-adapter:/, "") + || "legacy"; + + const hostRef = options.host_ref || createRef("host", "local"); + const runtimeRef = options.runtime_ref + || createRef("runtime", `native:${adapterType}`); + const endpointRef = options.endpoint_ref + || createRef("runtime-endpoint", endpointValue(hostRef, adapterType)); + + let health = null; + let availability = "unknown"; + if (options.check_health !== false) { + health = await port.health(); + if (health && health.ready === true) availability = "available"; + else if (health && health.ready === false) availability = "unavailable"; + } + + const endpoint = normalizeRuntimeEndpoint({ + endpoint_ref: endpointRef, + host_ref: hostRef, + runtime_ref: runtimeRef, + location: options.location || "local", + transport: options.transport || mapLegacyTransport(adapterMeta.transport), + availability, + descriptor: port.descriptor, + workspace_refs: options.workspace_refs || [], + }); + + return Object.freeze({ + endpoint, + port, + health, + adapter: adapterMeta, + }); +} + +module.exports = { + mapLegacyTransport, + discoverNativeRuntimeEndpoint, +}; From eb118c81d7199d0a6825beedcfda540a7d010298 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:20:50 +0800 Subject: [PATCH 069/166] test(m040): cover native RuntimeEndpoint discovery --- tests/runtime-port/native-endpoint.test.js | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/runtime-port/native-endpoint.test.js diff --git a/tests/runtime-port/native-endpoint.test.js b/tests/runtime-port/native-endpoint.test.js new file mode 100644 index 00000000..c853a5f7 --- /dev/null +++ b/tests/runtime-port/native-endpoint.test.js @@ -0,0 +1,94 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + discoverNativeRuntimeEndpoint, + mapLegacyTransport, +} = require(path.join(ROOT, "lib", "runtime-port", "native-endpoint.js")); + +function adapter(overrides = {}) { + return { + discover() { + return { + adapter_type: "codex", + version: "0.1.0", + transport: "stdio-json-rpc", + capabilities: ["text_generation"], + ...overrides.discover, + }; + }, + async health() { + return overrides.health || { status: "ok", ready: true }; + }, + async invoke() { + return { runId: "R-1", status: "ok", result: {} }; + }, + async cancel(runId) { + return { runId, cancelled: true }; + }, + async report(runId) { + return { runId, status: "ok", result: {} }; + }, + }; +} + +test("native adapter discovery maps to local HostRef and native RuntimeRef", async () => { + const result = await discoverNativeRuntimeEndpoint(adapter(), { + workspace_refs: ["workspace:WS-1"], + }); + + assert.equal(result.endpoint.host_ref, "host:local"); + assert.equal(result.endpoint.runtime_ref, "runtime:native:codex"); + assert.equal(result.endpoint.endpoint_ref, "runtime-endpoint:local:native:codex"); + assert.equal(result.endpoint.location, "local"); + assert.equal(result.endpoint.transport, "stdio"); + assert.equal(result.endpoint.availability, "available"); + assert.deepEqual(result.endpoint.workspace_refs, ["workspace:WS-1"]); +}); + +test("native endpoint availability comes from Adapter health", async () => { + const result = await discoverNativeRuntimeEndpoint(adapter({ + health: { status: "down", ready: false, error: "missing" }, + })); + assert.equal(result.endpoint.availability, "unavailable"); + assert.equal(result.health.error, "missing"); +}); + +test("observer-only adapter endpoint does not advertise create/cancel", async () => { + const result = await discoverNativeRuntimeEndpoint(adapter({ + discover: { + adapter_type: "cursor", + observer: true, + invoke_supported: false, + cancel_supported: false, + }, + })); + assert.equal(result.endpoint.runtime_ref, "runtime:native:cursor"); + assert.equal(result.endpoint.descriptor.capabilities.includes("runtime.run.create"), false); + assert.equal(result.endpoint.descriptor.capabilities.includes("runtime.run.cancel"), false); + assert.ok(result.endpoint.descriptor.capabilities.includes("runtime.run.status")); +}); + +test("health probing may be deferred without pretending availability", async () => { + let healthCalls = 0; + const value = adapter(); + value.health = async () => { + healthCalls += 1; + return { ready: true }; + }; + const result = await discoverNativeRuntimeEndpoint(value, { check_health: false }); + assert.equal(result.endpoint.availability, "unknown"); + assert.equal(result.health, null); + assert.equal(healthCalls, 0); +}); + +test("legacy transport mapping is conservative", () => { + assert.equal(mapLegacyTransport("stdio-json-rpc"), "stdio"); + assert.equal(mapLegacyTransport("https"), "http"); + assert.equal(mapLegacyTransport("websocket"), "websocket"); + assert.equal(mapLegacyTransport("vendor-magic"), "custom"); +}); From 5d589b47c7b1dce9308cfe0ee61ebb429f57c0fc Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:20:52 +0800 Subject: [PATCH 070/166] test(m040): include runtime topology suites --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c124854b..6045fada 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js" }, "keywords": [ "ai", From 3c24577fa1b177191f9bd687a0e417c71c400f6f Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:21:50 +0800 Subject: [PATCH 071/166] feat(m040): compose runtime filtering with host matcher --- lib/runtime-port/capability-router.js | 128 ++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 lib/runtime-port/capability-router.js diff --git a/lib/runtime-port/capability-router.js b/lib/runtime-port/capability-router.js new file mode 100644 index 00000000..19f6373a --- /dev/null +++ b/lib/runtime-port/capability-router.js @@ -0,0 +1,128 @@ +"use strict"; + +const { + filterRuntimeEndpoints, +} = require("../../packages/runtime-port/src/index.js"); +const { + matchExecutionSurface, +} = require("../runtime-adapters/execution-surface-matcher.js"); + +class RuntimeRoutingError extends Error { + constructor(code, details = {}) { + super(`[runtime-routing:${code}] ${JSON.stringify(details)}`); + this.name = "RuntimeRoutingError"; + this.code = code; + this.details = details; + } +} + +function normalizeBinding(input, index) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new RuntimeRoutingError("ERR_RUNTIME_HOST_BINDING_INVALID", { index }); + } + const keys = Object.keys(input); + for (const key of keys) { + if (!["endpoint_ref", "snapshot"].includes(key)) { + throw new RuntimeRoutingError("ERR_RUNTIME_HOST_BINDING_FIELD_UNKNOWN", { + index, + key, + }); + } + } + if (typeof input.endpoint_ref !== "string" || !input.endpoint_ref) { + throw new RuntimeRoutingError("ERR_RUNTIME_HOST_BINDING_ENDPOINT", { index }); + } + if (!input.snapshot || typeof input.snapshot !== "object" || Array.isArray(input.snapshot)) { + throw new RuntimeRoutingError("ERR_RUNTIME_HOST_BINDING_SNAPSHOT", { index }); + } + return Object.freeze({ + endpoint_ref: input.endpoint_ref, + snapshot: input.snapshot, + }); +} + +function routeRuntimeEndpoints(input, options = {}) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new RuntimeRoutingError("ERR_RUNTIME_ROUTING_INPUT", {}); + } + if (!input.host_requirement) { + throw new RuntimeRoutingError("ERR_HOST_REQUIREMENT_REQUIRED", {}); + } + + const runtimeFilter = filterRuntimeEndpoints( + input.runtime_requirement, + input.endpoints || [], + ); + const acceptedByRef = new Map( + runtimeFilter.accepted.map((endpoint) => [endpoint.endpoint_ref, endpoint]), + ); + + const bindings = (input.bindings || []).map(normalizeBinding); + const usableBindings = []; + const missingSnapshotEndpoints = []; + + for (const endpoint of runtimeFilter.accepted) { + const binding = bindings.find((item) => item.endpoint_ref === endpoint.endpoint_ref); + if (!binding) { + missingSnapshotEndpoints.push(endpoint.endpoint_ref); + continue; + } + usableBindings.push(binding); + } + + const profileOwners = new Map(); + for (const binding of usableBindings) { + const hostProfileRef = binding.snapshot.host_profile_ref; + if (typeof hostProfileRef !== "string" || !hostProfileRef) { + throw new RuntimeRoutingError("ERR_RUNTIME_HOST_PROFILE_REF_REQUIRED", { + endpoint_ref: binding.endpoint_ref, + }); + } + if (profileOwners.has(hostProfileRef)) { + throw new RuntimeRoutingError("ERR_RUNTIME_HOST_BINDING_AMBIGUOUS", { + host_profile_ref: hostProfileRef, + endpoints: [profileOwners.get(hostProfileRef), binding.endpoint_ref], + }); + } + profileOwners.set(hostProfileRef, binding.endpoint_ref); + } + + const hostPlan = matchExecutionSurface( + input.host_requirement, + usableBindings.map((item) => item.snapshot), + options, + ); + + let selection = null; + if (hostPlan.selection) { + const endpointRef = profileOwners.get(hostPlan.selection); + const endpoint = endpointRef ? acceptedByRef.get(endpointRef) : null; + if (!endpoint) { + throw new RuntimeRoutingError("ERR_RUNTIME_SELECTION_BINDING_MISSING", { + host_profile_ref: hostPlan.selection, + }); + } + selection = Object.freeze({ + endpoint_ref: endpoint.endpoint_ref, + host_ref: endpoint.host_ref, + runtime_ref: endpoint.runtime_ref, + host_profile_ref: hostPlan.selection, + }); + } + + return Object.freeze({ + runtime_filter: runtimeFilter, + missing_snapshot_endpoints: Object.freeze(missingSnapshotEndpoints), + host_plan: hostPlan, + selection, + authorization: Object.freeze({ + authorized: false, + reason: "routing_is_not_authorization", + }), + }); +} + +module.exports = { + RuntimeRoutingError, + routeRuntimeEndpoints, +}; From 87046e4a88ebe6b5e727354f7b524d408d897189 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:21:53 +0800 Subject: [PATCH 072/166] test(m040): cover composite runtime host routing --- tests/runtime-port/capability-router.test.js | 165 +++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 tests/runtime-port/capability-router.test.js diff --git a/tests/runtime-port/capability-router.test.js b/tests/runtime-port/capability-router.test.js new file mode 100644 index 00000000..f258fd5d --- /dev/null +++ b/tests/runtime-port/capability-router.test.js @@ -0,0 +1,165 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + routeRuntimeEndpoints, +} = require(path.join(ROOT, "lib", "runtime-port", "capability-router.js")); + +const NOW = "2026-09-29T04:00:00.000Z"; + +function endpoint(name, overrides = {}) { + return { + endpoint_ref: `runtime-endpoint:local:native:${name}`, + host_ref: "host:local", + runtime_ref: `runtime:native:${name}`, + location: "local", + transport: "stdio", + availability: "available", + descriptor: { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: `native-adapter:${name}`, + capabilities: [ + "runtime.discover", + "runtime.health", + "runtime.run.create", + "runtime.run.status", + ], + }, + workspace_refs: ["workspace:WS-1"], + ...overrides, + }; +} + +function snapshot(profile, reliability, overrides = {}) { + return { + schema_version: "1.0", + snapshot_id: `SNAP-${profile}`, + host_profile_ref: profile, + taken_at: "2026-09-29T03:59:00.000Z", + capabilities: { + "session.boundary": "native", + "tool.before.block": "native", + "tool.update": "adapter", + }, + governance: { approved: true, decision_id: "D-1" }, + lease: { active: true, holder: "owner" }, + reliability: { value: reliability, source: "explicit-workflow", quality: "high" }, + cost: { value: 0.2, source: "explicit-workflow", quality: "high" }, + latency: { value: 100, source: "explicit-workflow", quality: "high" }, + ...overrides, + }; +} + +function hostRequirement(overrides = {}) { + return { + schema_version: "1.0", + requirement_id: "HOST-REQ-1", + task_id: "T-1", + created_at: "2026-09-29T03:58:00.000Z", + required_capabilities: ["session.boundary", "tool.before.block"], + minimum_capability_levels: { "tool.before.block": "native" }, + governance: { + approved_decision_id: "D-1", + require_active_lease: true, + }, + preferred: {}, + ttl_at: "2026-09-29T05:00:00.000Z", + ...overrides, + }; +} + +function runtimeRequirement(overrides = {}) { + return { + requirement_id: "RUNTIME-REQ-1", + required_capabilities: ["runtime.run.create"], + workspace_ref: "workspace:WS-1", + ...overrides, + }; +} + +test("composite routing filters runtime capabilities then reuses existing host matcher", () => { + const result = routeRuntimeEndpoints({ + runtime_requirement: runtimeRequirement(), + endpoints: [endpoint("codex"), endpoint("claude-code")], + host_requirement: hostRequirement(), + bindings: [ + { endpoint_ref: "runtime-endpoint:local:native:codex", snapshot: snapshot("H-codex", 0.95) }, + { endpoint_ref: "runtime-endpoint:local:native:claude-code", snapshot: snapshot("H-claude", 0.7) }, + ], + }, { now: NOW }); + + assert.equal(result.selection.endpoint_ref, "runtime-endpoint:local:native:codex"); + assert.equal(result.selection.host_ref, "host:local"); + assert.equal(result.selection.runtime_ref, "runtime:native:codex"); + assert.equal(result.selection.host_profile_ref, "H-codex"); + assert.equal(result.authorization.authorized, false); + assert.equal(result.authorization.reason, "routing_is_not_authorization"); +}); + +test("explicit endpoint failure never silently falls back to another runtime", () => { + const result = routeRuntimeEndpoints({ + runtime_requirement: runtimeRequirement({ + endpoint_ref: "runtime-endpoint:local:native:claude-code", + }), + endpoints: [endpoint("codex"), endpoint("claude-code")], + host_requirement: hostRequirement(), + bindings: [ + { endpoint_ref: "runtime-endpoint:local:native:codex", snapshot: snapshot("H-codex", 0.95) }, + { + endpoint_ref: "runtime-endpoint:local:native:claude-code", + snapshot: snapshot("H-claude", 0.7, { + capabilities: { + "session.boundary": "native", + "tool.before.block": "unsupported", + }, + }), + }, + ], + }, { now: NOW }); + + assert.equal(result.selection, null); + assert.equal(result.runtime_filter.accepted.length, 1); + assert.equal( + result.runtime_filter.accepted[0].endpoint_ref, + "runtime-endpoint:local:native:claude-code", + ); + assert.ok(result.host_plan.candidates[0].rejected_reasons.some( + (reason) => reason.includes("tool.before.block"), + )); +}); + +test("endpoint with no host snapshot is visible but cannot be selected", () => { + const result = routeRuntimeEndpoints({ + runtime_requirement: runtimeRequirement(), + endpoints: [endpoint("codex")], + host_requirement: hostRequirement(), + bindings: [], + }, { now: NOW }); + + assert.equal(result.selection, null); + assert.deepEqual( + result.missing_snapshot_endpoints, + ["runtime-endpoint:local:native:codex"], + ); + assert.equal(result.host_plan.candidates.length, 0); +}); + +test("duplicate host profile bindings fail closed", () => { + assert.throws( + () => routeRuntimeEndpoints({ + runtime_requirement: runtimeRequirement(), + endpoints: [endpoint("codex"), endpoint("claude-code")], + host_requirement: hostRequirement(), + bindings: [ + { endpoint_ref: "runtime-endpoint:local:native:codex", snapshot: snapshot("H-shared", 0.9) }, + { endpoint_ref: "runtime-endpoint:local:native:claude-code", snapshot: snapshot("H-shared", 0.8) }, + ], + }, { now: NOW }), + (error) => error.code === "ERR_RUNTIME_HOST_BINDING_AMBIGUOUS", + ); +}); From fd3c6535a53834e6c118c2951359536740fd05e2 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:21:55 +0800 Subject: [PATCH 073/166] test(m040): include composite routing suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6045fada..f600a774 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js" }, "keywords": [ "ai", From 531a27a56640563a1f1893da46bb1b8c68309623 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:23:20 +0800 Subject: [PATCH 074/166] docs: define M-040 runtime topology routing --- docs/architecture/runtime-topology-routing.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/architecture/runtime-topology-routing.md diff --git a/docs/architecture/runtime-topology-routing.md b/docs/architecture/runtime-topology-routing.md new file mode 100644 index 00000000..7afe0ec7 --- /dev/null +++ b/docs/architecture/runtime-topology-routing.md @@ -0,0 +1,189 @@ +# Host / Runtime Topology and Capability Routing + +> **Status**: M-040 MS-005 baseline +> **Validated**: 2026-09-29 +> **Focused CI**: 59/59 PASS + +## 1. Identity model + +Cortex distinguishes three concepts that were historically easy to conflate: + +```text +HostRef + = physical/logical execution machine + e.g. host:local, host:mac-mini, host:linux-server + +RuntimeRef + = runtime implementation on a host + e.g. runtime:native:codex, runtime:paseo:mac-mini + +RuntimeEndpointRef + = one reachable runtime endpoint + e.g. runtime-endpoint:local:native:codex +``` + +Legacy `host_profile_ref` remains the coding-agent host/profile capability identity used by the existing execution-surface matcher. It is not a physical HostRef. + +## 2. Topology + +```mermaid +flowchart LR + H["HostRef
host:local"] --> E["RuntimeEndpoint
runtime-endpoint:local:native:codex"] + E --> R["RuntimeRef
runtime:native:codex"] + R --> P["RuntimePort"] + E -. explicit binding .-> HP["Legacy host_profile_ref
H-codex"] + HP --> M["Existing execution-surface matcher"] +``` + +Project topology remains independent: + +```text +Project topology + -> project identity / peers / repository relationships + +Runtime topology + -> host / runtime endpoint / execution capability +``` + +## 3. RuntimeEndpoint contract + +A RuntimeEndpoint carries: + +- endpoint_ref +- host_ref +- runtime_ref +- local/remote location +- transport +- availability +- runtime protocol descriptor +- available WorkspaceRefs + +The portable contract lives in `@cortex-agent/runtime-port`. + +Unknown fields fail closed. + +Runtime descriptors may declare only the `runtime.*` capability namespace. + +## 4. RuntimeRequirement + +A RuntimeRequirement can hard-filter by: + +- required runtime capabilities +- explicit endpoint +- HostRef +- RuntimeRef +- location +- transport +- WorkspaceRef +- availability + +The filter is deterministic and has no soft score. + +## 5. Native adapter discovery + +Existing native adapters are projected into RuntimeEndpoint records. + +Example: + +```text +Codex Adapter + -> HostRef: host:local + -> RuntimeRef: runtime:native:codex + -> Endpoint: runtime-endpoint:local:native:codex +``` + +Adapter `health()` supplies endpoint availability when probing is enabled. + +Observer-only adapters do not gain execution capabilities merely because BaseAdapter methods exist. + +## 6. Two-stage routing + +Cortex reuses the existing host matcher rather than replacing it. + +```text +RuntimeRequirement + | + v +RuntimeEndpoint hard filter + | + v +accepted RuntimeEndpoints + | + +-- explicit endpoint ↔ host_profile_ref snapshot binding + | + v +existing execution-surface-matcher + - host capability level + - governance decision + - lease + - TTL + - reliability/cost/latency advisory score + | + v +RuntimeEndpoint selection +``` + +The composite router lives at `lib/runtime-port/capability-router.js`. + +## 7. No silent failover + +When a caller specifies an exact RuntimeEndpoint, all other endpoints fail the runtime hard filter. + +If the requested endpoint later fails the host/governance matcher, routing returns: + +```text +selection = null +``` + +It does not silently execute elsewhere. + +This is especially important for risky operations. + +## 8. Routing is not authorization + +Every composite routing result explicitly carries: + +```json +{ + "authorization": { + "authorized": false, + "reason": "routing_is_not_authorization" + } +} +``` + +Authorization remains with Decisions, Waitpoints, workflow gates, leases and operation lifecycle. + +## 9. Existing routing investments preserved + +M-040 keeps these existing owners: + +- `lib/runtime-adapters/capability-contract.js` +- `host-runtime-snapshots.js` +- `execution-surface-matcher.js` +- `dispatch-policy.js` +- operation lifecycle / authorization owners + +Runtime topology is a new portable layer above them, not a replacement routing engine. + +## 10. Validation + +Focused validation covers: + +- observer-only adapter capability correctness; +- undeclared RuntimePort operations fail closed; +- HostRef / RuntimeRef / RuntimeEndpointRef separation; +- runtime-only capability namespace; +- availability and workspace hard filters; +- explicit endpoint no-fallback; +- native endpoint health mapping; +- composite routing through the existing matcher; +- missing/ambiguous endpoint-to-host-profile binding safety. + +Result: + +```text +59 tests +59 pass +0 fail +``` From 47d8147ed46276788c7f1492bfd2649ca3e8e82f Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:23:23 +0800 Subject: [PATCH 075/166] docs: index runtime topology architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 525ec25f..3a7d7cc7 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -30,6 +30,7 @@ - [Protocol, SDK and Progressive Monorepo](./protocol-sdk-monorepo.md) - [Protocol Versioning and Capability Negotiation](./protocol-negotiation.md) - [RuntimePort v1 and Native Adapter Compatibility](./runtime-port-v1.md) +- [Host / Runtime Topology and Capability Routing](./runtime-topology-routing.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 27ee481bdf1d7f88cea937870b3651df5042bff8 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:41 +0800 Subject: [PATCH 076/166] feat(m040): add governed Control Service core --- lib/control-service/service.js | 139 +++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 lib/control-service/service.js diff --git a/lib/control-service/service.js b/lib/control-service/service.js new file mode 100644 index 00000000..91d386bf --- /dev/null +++ b/lib/control-service/service.js @@ -0,0 +1,139 @@ +"use strict"; + +class ControlServiceError extends Error { + constructor(code, details = {}) { + super(`[control-service:${code}] ${JSON.stringify(details)}`); + this.name = "ControlServiceError"; + this.code = code; + this.details = details; + } +} + +function requireFunction(value, name) { + if (typeof value !== "function") { + throw new ControlServiceError("ERR_CONTROL_DEPENDENCY_REQUIRED", { dependency: name }); + } + return value; +} + +function validateRequest(input, { execution = false } = {}) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new ControlServiceError("ERR_CONTROL_REQUEST_INVALID", {}); + } + if (typeof input.task_id !== "string" || !input.task_id.trim()) { + throw new ControlServiceError("ERR_CONTROL_TASK_REQUIRED", {}); + } + if (execution && (typeof input.idempotency_key !== "string" || !input.idempotency_key.trim())) { + throw new ControlServiceError("ERR_CONTROL_IDEMPOTENCY_REQUIRED", {}); + } + return input; +} + +function createControlService(dependencies = {}) { + const resolvePlan = requireFunction(dependencies.resolvePlan, "resolvePlan"); + const route = requireFunction(dependencies.route, "route"); + const authorize = requireFunction(dependencies.authorize, "authorize"); + const dispatch = requireFunction(dependencies.dispatch, "dispatch"); + + function inspect(input) { + const request = validateRequest(input); + const plan = resolvePlan(request); + + if (!plan || plan.would_proceed !== true) { + return Object.freeze({ + status: "blocked", + reason: "dispatch_plan_blocked", + task_id: request.task_id, + plan: plan || null, + routing: null, + selection: null, + authorization: Object.freeze({ authorized: false, checked: false }), + }); + } + + const routing = route(request); + if (!routing || !routing.selection) { + return Object.freeze({ + status: "blocked", + reason: "no_runtime_selection", + task_id: request.task_id, + plan, + routing: routing || null, + selection: null, + authorization: Object.freeze({ authorized: false, checked: false }), + }); + } + + return Object.freeze({ + status: "ready", + reason: null, + task_id: request.task_id, + plan, + routing, + selection: routing.selection, + authorization: Object.freeze({ authorized: false, checked: false }), + }); + } + + async function execute(input) { + const request = validateRequest(input, { execution: true }); + const inspection = inspect(request); + if (inspection.status !== "ready") return inspection; + + const auth = await authorize(Object.freeze({ + task_id: request.task_id, + idempotency_key: request.idempotency_key, + selection: inspection.selection, + plan: inspection.plan, + workflow_gate: request.workflow_gate || null, + })); + + if (!auth || auth.authorized !== true) { + return Object.freeze({ + ...inspection, + status: "awaiting_authorization", + reason: auth && auth.reason ? auth.reason : "authorization_required", + authorization: Object.freeze({ + authorized: false, + checked: true, + authorization_ref: null, + }), + }); + } + + if (typeof auth.authorization_ref !== "string" || !auth.authorization_ref.trim()) { + throw new ControlServiceError("ERR_CONTROL_AUTHORIZATION_REF_REQUIRED", { + task_id: request.task_id, + }); + } + + const result = await dispatch(Object.freeze({ + task_id: request.task_id, + idempotency_key: request.idempotency_key, + selection: inspection.selection, + authorization_ref: auth.authorization_ref, + workflow_gate: request.workflow_gate || null, + plan: inspection.plan, + })); + + return Object.freeze({ + ...inspection, + status: "dispatched", + reason: null, + authorization: Object.freeze({ + authorized: true, + checked: true, + authorization_ref: auth.authorization_ref, + }), + dispatch_result: result, + }); + } + + return Object.freeze({ inspect, execute }); +} + +module.exports = { + ControlServiceError, + createControlService, + validateRequest, +}; From bad5f2e3e6dbd42bf82029a095e7ee44e2b05384 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:45 +0800 Subject: [PATCH 077/166] feat(m040): compose local Control Service owners --- lib/control-service/local.js | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/control-service/local.js diff --git a/lib/control-service/local.js b/lib/control-service/local.js new file mode 100644 index 00000000..9e751fd9 --- /dev/null +++ b/lib/control-service/local.js @@ -0,0 +1,50 @@ +"use strict"; + +const { resolveDispatchPlan } = require("../dispatch/plan.js"); +const { routeRuntimeEndpoints } = require("../runtime-port/capability-router.js"); +const { createControlService } = require("./service.js"); + +function createLocalControlService(options = {}) { + if (typeof options.authorize !== "function") { + const error = new Error("createLocalControlService requires an authorize owner"); + error.code = "ERR_CONTROL_AUTHORIZE_OWNER_REQUIRED"; + throw error; + } + if (typeof options.dispatch !== "function") { + const error = new Error("createLocalControlService requires a dispatch owner"); + error.code = "ERR_CONTROL_DISPATCH_OWNER_REQUIRED"; + throw error; + } + + const clock = typeof options.clock === "function" + ? options.clock + : () => new Date().toISOString(); + + return createControlService({ + resolvePlan(request) { + return resolveDispatchPlan( + request.project_root || process.cwd(), + request.task_id, + { now: request.now || clock() }, + ); + }, + + route(request) { + return routeRuntimeEndpoints({ + runtime_requirement: request.runtime_requirement, + endpoints: request.endpoints || [], + host_requirement: request.host_requirement, + bindings: request.bindings || [], + }, { + now: request.now || clock(), + }); + }, + + authorize: options.authorize, + dispatch: options.dispatch, + }); +} + +module.exports = { + createLocalControlService, +}; From 52515bf88c017b1484f71d4ab070f2da6645f70a Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:47 +0800 Subject: [PATCH 078/166] test(m040): cover Control Service gates --- tests/control-service/control-service.test.js | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/control-service/control-service.test.js diff --git a/tests/control-service/control-service.test.js b/tests/control-service/control-service.test.js new file mode 100644 index 00000000..63265688 --- /dev/null +++ b/tests/control-service/control-service.test.js @@ -0,0 +1,140 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + createControlService, +} = require(path.join(ROOT, "lib", "control-service", "service.js")); + +function readyRouting() { + return { + selection: { + endpoint_ref: "runtime-endpoint:local:native:codex", + host_ref: "host:local", + runtime_ref: "runtime:native:codex", + host_profile_ref: "H-codex", + }, + }; +} + +test("blocked dispatch plan short-circuits routing and all mutation owners", async () => { + const calls = []; + const service = createControlService({ + resolvePlan() { + calls.push("plan"); + return { would_proceed: false, errors: ["locked"] }; + }, + route() { calls.push("route"); return readyRouting(); }, + authorize() { calls.push("authorize"); return { authorized: true, authorization_ref: "decision:D-1" }; }, + dispatch() { calls.push("dispatch"); return {}; }, + }); + + const result = await service.execute({ task_id: "T-1", idempotency_key: "K-1" }); + assert.equal(result.status, "blocked"); + assert.equal(result.reason, "dispatch_plan_blocked"); + assert.deepEqual(calls, ["plan"]); +}); + +test("no runtime selection short-circuits authorization and dispatch", async () => { + const calls = []; + const service = createControlService({ + resolvePlan() { calls.push("plan"); return { would_proceed: true }; }, + route() { calls.push("route"); return { selection: null }; }, + authorize() { calls.push("authorize"); return { authorized: true, authorization_ref: "decision:D-1" }; }, + dispatch() { calls.push("dispatch"); return {}; }, + }); + + const result = await service.execute({ task_id: "T-1", idempotency_key: "K-1" }); + assert.equal(result.status, "blocked"); + assert.equal(result.reason, "no_runtime_selection"); + assert.deepEqual(calls, ["plan", "route"]); +}); + +test("authorization denial never reaches dispatcher", async () => { + const calls = []; + const service = createControlService({ + resolvePlan() { return { would_proceed: true }; }, + route() { return readyRouting(); }, + authorize(input) { + calls.push(["authorize", input]); + return { authorized: false, reason: "decision_open" }; + }, + dispatch() { calls.push(["dispatch"]); return {}; }, + }); + + const result = await service.execute({ + task_id: "T-1", + idempotency_key: "K-1", + workflow_gate: "mission", + }); + assert.equal(result.status, "awaiting_authorization"); + assert.equal(result.reason, "decision_open"); + assert.equal(result.authorization.authorized, false); + assert.equal(calls.length, 1); +}); + +test("authorized execution requires an evidence reference", async () => { + const service = createControlService({ + resolvePlan() { return { would_proceed: true }; }, + route() { return readyRouting(); }, + authorize() { return { authorized: true }; }, + dispatch() { throw new Error("must not dispatch"); }, + }); + + await assert.rejects( + () => service.execute({ task_id: "T-1", idempotency_key: "K-1" }), + (error) => error.code === "ERR_CONTROL_AUTHORIZATION_REF_REQUIRED", + ); +}); + +test("authorized request delegates exactly once with selected endpoint and authorization ref", async () => { + const dispatched = []; + const service = createControlService({ + resolvePlan() { return { would_proceed: true, plan_id: "P-1" }; }, + route() { return readyRouting(); }, + authorize() { + return { + authorized: true, + authorization_ref: "decision:D-1", + }; + }, + dispatch(input) { + dispatched.push(input); + return { ok: true, run_ref: "run:R-1" }; + }, + }); + + const result = await service.execute({ + task_id: "T-1", + idempotency_key: "K-1", + workflow_gate: "mission", + }); + + assert.equal(result.status, "dispatched"); + assert.equal(result.authorization.authorization_ref, "decision:D-1"); + assert.equal(result.dispatch_result.run_ref, "run:R-1"); + assert.equal(dispatched.length, 1); + assert.equal(dispatched[0].selection.endpoint_ref, "runtime-endpoint:local:native:codex"); + assert.equal(dispatched[0].authorization_ref, "decision:D-1"); + assert.equal(dispatched[0].idempotency_key, "K-1"); +}); + +test("inspect is read/coordination-only and never calls authorization or dispatch", () => { + let authorizeCalls = 0; + let dispatchCalls = 0; + const service = createControlService({ + resolvePlan() { return { would_proceed: true }; }, + route() { return readyRouting(); }, + authorize() { authorizeCalls += 1; return { authorized: true, authorization_ref: "D-1" }; }, + dispatch() { dispatchCalls += 1; return {}; }, + }); + + const result = service.inspect({ task_id: "T-1" }); + assert.equal(result.status, "ready"); + assert.equal(result.authorization.checked, false); + assert.equal(authorizeCalls, 0); + assert.equal(dispatchCalls, 0); +}); From 729df106347883a99aa7658ed4b9696db648bdc4 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:50 +0800 Subject: [PATCH 079/166] test(m040): guard Control Service ownership boundary --- .../control-service/control-boundary.test.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/control-service/control-boundary.test.js diff --git a/tests/control-service/control-boundary.test.js b/tests/control-service/control-boundary.test.js new file mode 100644 index 00000000..4ef82eda --- /dev/null +++ b/tests/control-service/control-boundary.test.js @@ -0,0 +1,35 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); + +test("Control Service core owns no filesystem/process/network persistence", () => { + const text = fs.readFileSync( + path.join(ROOT, "lib", "control-service", "service.js"), + "utf8", + ); + for (const forbidden of [ + "node:fs", + "child_process", + "node:http", + "node:https", + ".agent/", + ".agent-runtime/", + ]) { + assert.equal(text.includes(forbidden), false, `control core must not contain ${forbidden}`); + } +}); + +test("daemon and trigger public CLI remain Phase 0 fail-closed while Control Service core stabilizes", () => { + const contract = require(path.join(ROOT, "lib", "cli", "contract.js")); + for (const name of ["daemon", "trigger"]) { + const entry = contract.commands.find((item) => item.name === name); + assert.equal(entry.mode, "phase0_stub"); + assert.equal(entry.implemented, false); + assert.equal(entry.default_enabled === false || name === "trigger", true); + } +}); From b44cb1e17636a04911b5c1f5e4d399990bcb1ef3 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:54 +0800 Subject: [PATCH 080/166] test(m040): include Control Service suites --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f600a774..85fc5fbd 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js" }, "keywords": [ "ai", From 2aa1dd4d01b3e9f61a0f879658bfee2bda8f7a47 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:26:36 +0800 Subject: [PATCH 081/166] test(m040): cover local Control Service composition --- .../local-control-service.test.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/control-service/local-control-service.test.js diff --git a/tests/control-service/local-control-service.test.js b/tests/control-service/local-control-service.test.js new file mode 100644 index 00000000..8c08a971 --- /dev/null +++ b/tests/control-service/local-control-service.test.js @@ -0,0 +1,115 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + createLocalControlService, +} = require(path.join(ROOT, "lib", "control-service", "local.js")); + +function mkProject() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "m040-control-")); + for (const sub of ["runs", "queues", "sessions", "decisions", "waitpoints", "locks"]) { + fs.mkdirSync(path.join(root, ".agent", sub), { recursive: true }); + } + return root; +} + +function endpoint() { + return { + endpoint_ref: "runtime-endpoint:local:native:codex", + host_ref: "host:local", + runtime_ref: "runtime:native:codex", + location: "local", + transport: "stdio", + availability: "available", + descriptor: { + protocol: "cortex-runtime", + protocol_version: "1.0", + implementation: "native-adapter:codex", + capabilities: ["runtime.run.create"], + }, + workspace_refs: [], + }; +} + +function hostRequirement(now) { + return { + schema_version: "1.0", + requirement_id: "HOST-CONTROL-1", + task_id: "T-CONTROL", + created_at: now, + required_capabilities: ["session.boundary", "tool.before.block"], + minimum_capability_levels: { "tool.before.block": "native" }, + governance: { + approved_decision_id: null, + require_active_lease: false, + }, + preferred: {}, + ttl_at: new Date(new Date(now).getTime() + 60_000).toISOString(), + }; +} + +function snapshot(now) { + return { + schema_version: "1.0", + snapshot_id: "SNAP-CONTROL", + host_profile_ref: "H-codex", + taken_at: now, + capabilities: { + "session.boundary": "native", + "tool.before.block": "native", + }, + governance: { approved: true, decision_id: null }, + lease: { active: true, holder: "owner" }, + reliability: { value: 1, source: "explicit-workflow", quality: "high" }, + cost: { value: 0, source: "explicit-workflow", quality: "high" }, + latency: { value: 1, source: "explicit-workflow", quality: "high" }, + }; +} + +test("local Control Service composes existing dispatch plan and runtime router without owning state", async () => { + const root = mkProject(); + const now = "2026-09-29T04:30:00.000Z"; + let dispatchCalls = 0; + try { + const service = createLocalControlService({ + clock: () => now, + authorize() { + return { authorized: false, reason: "approval_required" }; + }, + dispatch() { + dispatchCalls += 1; + return { ok: true }; + }, + }); + + const result = await service.execute({ + project_root: root, + task_id: "T-CONTROL", + idempotency_key: "T-CONTROL:1", + now, + runtime_requirement: { + requirement_id: "RUNTIME-CONTROL-1", + required_capabilities: ["runtime.run.create"], + }, + endpoints: [endpoint()], + host_requirement: hostRequirement(now), + bindings: [{ + endpoint_ref: "runtime-endpoint:local:native:codex", + snapshot: snapshot(now), + }], + }); + + assert.equal(result.status, "awaiting_authorization"); + assert.equal(result.selection.endpoint_ref, "runtime-endpoint:local:native:codex"); + assert.equal(result.plan.mutation_evidence.mutated_count, 0); + assert.equal(dispatchCalls, 0); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); From 51b399f71bad5b2bb5b14c6171829d7562bc3b1f Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:26:39 +0800 Subject: [PATCH 082/166] ci(m040): validate Control Service changes --- .github/workflows/m040-validation.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml index 188175be..303ea9b0 100644 --- a/.github/workflows/m040-validation.yml +++ b/.github/workflows/m040-validation.yml @@ -11,11 +11,13 @@ on: - "packages/**" - "lib/sdk/**" - "lib/runtime-port/**" + - "lib/control-service/**" - "lib/commands/management/query.js" - "tests/architecture/**" - "tests/protocol/**" - "tests/sdk/**" - "tests/runtime-port/**" + - "tests/control-service/**" - "tests/management/management-query-cli.test.js" - ".github/workflows/m040-validation.yml" pull_request: From 2481b5c53066a78a672214f5758579716c42e7e7 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:26:41 +0800 Subject: [PATCH 083/166] test(m040): include local Control Service suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 85fc5fbd..75af439f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js" }, "keywords": [ "ai", From 1351df108539cdee2eb70c4204e0c331d312a4e9 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:28:01 +0800 Subject: [PATCH 084/166] docs: define M-040 Control Service boundary --- docs/architecture/control-service-boundary.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/architecture/control-service-boundary.md diff --git a/docs/architecture/control-service-boundary.md b/docs/architecture/control-service-boundary.md new file mode 100644 index 00000000..f8f49a9b --- /dev/null +++ b/docs/architecture/control-service-boundary.md @@ -0,0 +1,158 @@ +# Control Service and Daemon Boundary + +> **Status**: M-040 MS-006 baseline +> **Validated**: 2026-09-29 +> **Focused CI**: 68/68 PASS + +## 1. Position + +The Cortex Control Service is an optional coordination layer. + +It is not: + +- a source of Mission truth; +- a new Coordination state machine; +- an Agent Runtime; +- an authorization engine; +- a persistence owner. + +Its job is to compose existing owners in the correct order. + +## 2. Control flow + +```text +ControlService.inspect() + -> existing DispatchPlan resolver + -> RuntimeEndpoint routing + -> ready | blocked + +ControlService.execute() + -> inspect() + -> explicit authorize() owner + -> require authorization_ref + -> explicit dispatch() owner + -> dispatched +``` + +The Control Service itself does not read or write `.agent` directly. + +## 3. Owner injection + +The core requires four explicit dependencies: + +- resolvePlan +- route +- authorize +- dispatch + +This prevents the coordinator from becoming an implicit owner of any of those concerns. + +The local composition binds only: + +- existing `lib/dispatch/plan.js`; +- existing RuntimeEndpoint/host routing. + +Authorization and dispatch owners must still be supplied explicitly. + +## 4. Fail-closed ordering + +Execution is short-circuited in this order: + +```text +dispatch plan blocked + -> stop + +no runtime selection + -> stop + +authorization denied/missing + -> stop + +authorization has no evidence ref + -> fail closed + +only then + -> dispatcher +``` + +An authorized result must include an `authorization_ref` such as a Decision evidence reference. + +## 5. No second state store + +`lib/control-service/service.js` contains no filesystem, process, network or `.agent` persistence code. + +All mutation remains with existing owners. + +The local integration test confirms the existing dispatch plan reports zero mutations before authorization. + +## 6. Daemon boundary + +A daemon is a hosting mode for repeated Control Service iterations, not a new architecture layer. + +The existing daemon/trigger schemas remain useful contracts, but the public CLI remains fail-closed: + +```text +cortex-agent daemon ... +cortex-agent trigger ... +=> Phase 0 stub / disabled +``` + +This is intentional for M-040 MS-006. + +The Control Service core must stabilize before Cortex enables a persistent polling process. Enabling a daemon later requires an explicit opt-in lifecycle and must preserve: + +- workflow/Decision/Waitpoint gates; +- existing queue/session/run owners; +- idempotency; +- recoverable daemon state; +- default-disabled behavior; +- stop controls. + +## 7. Trigger semantics + +A Trigger remains a request to evaluate work, never execution authority. + +```text +Trigger + -> Control Service evaluation + -> DispatchPlan + -> Runtime routing + -> Authorization + -> Dispatcher +``` + +No Trigger type can bypass the authorization stage. + +## 8. Direct mode remains valid + +Cortex continues to support direct governed execution without a daemon. + +```text +workflow / CLI + -> governed dispatch / Control Service call + -> RuntimePort +``` + +A future daemon merely repeats the same governed iteration. + +## 9. Validation + +Focused tests verify: + +- blocked plan never reaches routing/authorization/dispatch; +- missing runtime selection never reaches authorization/dispatch; +- authorization denial never reaches dispatcher; +- successful authorization requires an evidence reference; +- dispatcher receives the selected RuntimeEndpoint and authorization ref; +- inspect is coordination-only; +- local composition reuses existing read-only plan/routing owners; +- Control Service core contains no persistence/process/network ownership; +- daemon and trigger public CLI remain default-disabled stubs. + +Result: + +```text +68 tests +68 pass +0 fail +``` From 06d55f0c193e4f2f25ae0afa3c3b533866d0703d Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:28:04 +0800 Subject: [PATCH 085/166] docs: index Control Service architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 3a7d7cc7..c7e0d870 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -31,6 +31,7 @@ - [Protocol Versioning and Capability Negotiation](./protocol-negotiation.md) - [RuntimePort v1 and Native Adapter Compatibility](./runtime-port-v1.md) - [Host / Runtime Topology and Capability Routing](./runtime-topology-routing.md) +- [Control Service and Daemon Boundary](./control-service-boundary.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 46aa2a9df6bbd55115a13d5b25e9e70a0bc64115 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:30:30 +0800 Subject: [PATCH 086/166] feat(m040): define CortexEvent v1 --- packages/protocol/src/events.js | 225 ++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 packages/protocol/src/events.js diff --git a/packages/protocol/src/events.js b/packages/protocol/src/events.js new file mode 100644 index 00000000..b8ebd927 --- /dev/null +++ b/packages/protocol/src/events.js @@ -0,0 +1,225 @@ +"use strict"; + +const { isRef } = require("./refs"); + +const CORTEX_EVENT_SCHEMA_VERSION = "1"; +const CORTEX_EVENT_TYPE_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/; +const SOURCE_KINDS = Object.freeze([ + "framework", + "coordination", + "runtime", + "project", + "extension", + "control", +]); + +const TOP_KEYS = new Set([ + "schema_version", + "event_id", + "type", + "occurred_at", + "source", + "correlation", + "sequence", + "causation_id", + "payload", + "evidence_refs", + "redacted", +]); + +const SOURCE_KEYS = new Set([ + "kind", + "source_event_id", + "producer_id", + "producer_kind", + "project_ref", + "host_ref", + "runtime_ref", +]); + +const CORRELATION_KEYS = new Set([ + "mission_id", + "milestone_id", + "task_id", + "decision_id", + "waitpoint_id", + "operation_id", + "trace_id", + "correlation_id", + "run_ref", + "session_ref", + "workspace_ref", +]); + +const SEQUENCE_KEYS = new Set(["stream_id", "value"]); + +class CortexEventError extends Error { + constructor(code, details = {}) { + super(`[cortex-event:${code}] ${JSON.stringify(details)}`); + this.name = "CortexEventError"; + this.code = code; + this.details = details; + } +} + +function plain(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function rejectUnknownKeys(value, known, where) { + for (const key of Object.keys(value || {})) { + if (!known.has(key)) { + throw new CortexEventError("ERR_CORTEX_EVENT_FIELD_UNKNOWN", { where, key }); + } + } +} + +function requiredString(value, where) { + if (typeof value !== "string" || !value.trim() || /[\r\n]/.test(value)) { + throw new CortexEventError("ERR_CORTEX_EVENT_FIELD_INVALID", { where }); + } + return value.trim(); +} + +function optionalString(value, where) { + if (value == null) return null; + return requiredString(value, where); +} + +function timestamp(value) { + const raw = requiredString(value, "occurred_at"); + const parsed = new Date(raw); + if (Number.isNaN(parsed.getTime())) { + throw new CortexEventError("ERR_CORTEX_EVENT_TIMESTAMP", { value }); + } + return parsed.toISOString(); +} + +function optionalRef(value, kind, where) { + if (value == null) return null; + if (!isRef(value, kind)) { + throw new CortexEventError("ERR_CORTEX_EVENT_REF", { where, kind, value }); + } + return value; +} + +function normalizeSource(input) { + if (!plain(input)) { + throw new CortexEventError("ERR_CORTEX_EVENT_SOURCE", {}); + } + rejectUnknownKeys(input, SOURCE_KEYS, "source"); + if (!SOURCE_KINDS.includes(input.kind)) { + throw new CortexEventError("ERR_CORTEX_EVENT_SOURCE_KIND", { kind: input.kind }); + } + return Object.freeze({ + kind: input.kind, + source_event_id: requiredString(input.source_event_id, "source.source_event_id"), + producer_id: optionalString(input.producer_id, "source.producer_id"), + producer_kind: optionalString(input.producer_kind, "source.producer_kind"), + project_ref: optionalRef(input.project_ref, "project", "source.project_ref"), + host_ref: optionalRef(input.host_ref, "host", "source.host_ref"), + runtime_ref: optionalRef(input.runtime_ref, "runtime", "source.runtime_ref"), + }); +} + +function normalizeCorrelation(input) { + if (input == null) input = {}; + if (!plain(input)) { + throw new CortexEventError("ERR_CORTEX_EVENT_CORRELATION", {}); + } + rejectUnknownKeys(input, CORRELATION_KEYS, "correlation"); + const out = { + mission_id: optionalString(input.mission_id, "correlation.mission_id"), + milestone_id: optionalString(input.milestone_id, "correlation.milestone_id"), + task_id: optionalString(input.task_id, "correlation.task_id"), + decision_id: optionalString(input.decision_id, "correlation.decision_id"), + waitpoint_id: optionalString(input.waitpoint_id, "correlation.waitpoint_id"), + operation_id: optionalString(input.operation_id, "correlation.operation_id"), + trace_id: optionalString(input.trace_id, "correlation.trace_id"), + correlation_id: optionalString(input.correlation_id, "correlation.correlation_id"), + run_ref: optionalRef(input.run_ref, "run", "correlation.run_ref"), + session_ref: optionalRef(input.session_ref, "session", "correlation.session_ref"), + workspace_ref: optionalRef(input.workspace_ref, "workspace", "correlation.workspace_ref"), + }; + return Object.freeze(out); +} + +function normalizeSequence(input) { + if (input == null) return null; + if (!plain(input)) { + throw new CortexEventError("ERR_CORTEX_EVENT_SEQUENCE", {}); + } + rejectUnknownKeys(input, SEQUENCE_KEYS, "sequence"); + const value = input.value; + if (!Number.isSafeInteger(value) || value < 0) { + throw new CortexEventError("ERR_CORTEX_EVENT_SEQUENCE_VALUE", { value }); + } + return Object.freeze({ + stream_id: requiredString(input.stream_id, "sequence.stream_id"), + value, + }); +} + +function normalizeCortexEvent(input) { + if (!plain(input)) { + throw new CortexEventError("ERR_CORTEX_EVENT_INVALID", {}); + } + rejectUnknownKeys(input, TOP_KEYS, "event"); + + const schemaVersion = input.schema_version == null + ? CORTEX_EVENT_SCHEMA_VERSION + : String(input.schema_version); + if (schemaVersion !== CORTEX_EVENT_SCHEMA_VERSION) { + throw new CortexEventError("ERR_CORTEX_EVENT_SCHEMA_VERSION", { + expected: CORTEX_EVENT_SCHEMA_VERSION, + received: schemaVersion, + }); + } + + const type = requiredString(input.type, "type"); + if (!CORTEX_EVENT_TYPE_PATTERN.test(type)) { + throw new CortexEventError("ERR_CORTEX_EVENT_TYPE", { type }); + } + if (!plain(input.payload)) { + throw new CortexEventError("ERR_CORTEX_EVENT_PAYLOAD", {}); + } + if (typeof input.redacted !== "boolean") { + throw new CortexEventError("ERR_CORTEX_EVENT_REDACTION", {}); + } + const evidence = input.evidence_refs == null ? [] : input.evidence_refs; + if (!Array.isArray(evidence)) { + throw new CortexEventError("ERR_CORTEX_EVENT_EVIDENCE", {}); + } + + return Object.freeze({ + schema_version: CORTEX_EVENT_SCHEMA_VERSION, + event_id: requiredString(input.event_id, "event_id"), + type, + occurred_at: timestamp(input.occurred_at), + source: normalizeSource(input.source), + correlation: normalizeCorrelation(input.correlation), + sequence: normalizeSequence(input.sequence), + causation_id: optionalString(input.causation_id, "causation_id"), + payload: Object.freeze({ ...input.payload }), + evidence_refs: Object.freeze(evidence.map((value, index) => + requiredString(value, `evidence_refs[${index}]`))), + redacted: input.redacted, + }); +} + +function canonicalEventId(sourceKind, sourceEventId) { + const kind = requiredString(sourceKind, "sourceKind"); + if (!SOURCE_KINDS.includes(kind)) { + throw new CortexEventError("ERR_CORTEX_EVENT_SOURCE_KIND", { kind }); + } + return `${kind}:${requiredString(sourceEventId, "sourceEventId")}`; +} + +module.exports = { + CORTEX_EVENT_SCHEMA_VERSION, + CORTEX_EVENT_TYPE_PATTERN, + SOURCE_KINDS, + CortexEventError, + canonicalEventId, + normalizeCortexEvent, +}; From 60ee6996e4133ceaa65c5ca9052d96ab7fffc519 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:30:32 +0800 Subject: [PATCH 087/166] feat(m040): add timeline dedupe gap cursor contract --- packages/protocol/src/timeline.js | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 packages/protocol/src/timeline.js diff --git a/packages/protocol/src/timeline.js b/packages/protocol/src/timeline.js new file mode 100644 index 00000000..855a5a8b --- /dev/null +++ b/packages/protocol/src/timeline.js @@ -0,0 +1,115 @@ +"use strict"; + +const { normalizeCortexEvent, CortexEventError } = require("./events"); + +const TIMELINE_CURSOR_SCHEMA_VERSION = "1"; +const CURSOR_KEYS = new Set(["schema_version", "stream_id", "position", "event_id"]); + +function normalizeTimelineCursor(input) { + if (input == null) return null; + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new CortexEventError("ERR_TIMELINE_CURSOR_INVALID", {}); + } + for (const key of Object.keys(input)) { + if (!CURSOR_KEYS.has(key)) { + throw new CortexEventError("ERR_TIMELINE_CURSOR_FIELD_UNKNOWN", { key }); + } + } + const schemaVersion = input.schema_version == null + ? TIMELINE_CURSOR_SCHEMA_VERSION + : String(input.schema_version); + if (schemaVersion !== TIMELINE_CURSOR_SCHEMA_VERSION) { + throw new CortexEventError("ERR_TIMELINE_CURSOR_SCHEMA_VERSION", { + received: schemaVersion, + }); + } + if (typeof input.stream_id !== "string" || !input.stream_id.trim()) { + throw new CortexEventError("ERR_TIMELINE_CURSOR_STREAM", {}); + } + if (typeof input.position !== "string" || !input.position.trim()) { + throw new CortexEventError("ERR_TIMELINE_CURSOR_POSITION", {}); + } + if (input.event_id != null && (typeof input.event_id !== "string" || !input.event_id.trim())) { + throw new CortexEventError("ERR_TIMELINE_CURSOR_EVENT", {}); + } + return Object.freeze({ + schema_version: TIMELINE_CURSOR_SCHEMA_VERSION, + stream_id: input.stream_id.trim(), + position: input.position.trim(), + event_id: input.event_id == null ? null : input.event_id.trim(), + }); +} + +function analyzeCortexTimeline(eventInputs, options = {}) { + if (!Array.isArray(eventInputs)) { + throw new CortexEventError("ERR_TIMELINE_EVENTS_INVALID", {}); + } + const events = []; + const duplicates = []; + const seen = new Set(); + const lastByStream = new Map(Object.entries(options.initial_sequences || {})); + const gaps = []; + + for (const raw of eventInputs) { + const event = normalizeCortexEvent(raw); + if (seen.has(event.event_id)) { + duplicates.push(event.event_id); + continue; + } + seen.add(event.event_id); + events.push(event); + + if (!event.sequence) continue; + const stream = event.sequence.stream_id; + const current = event.sequence.value; + if (lastByStream.has(stream)) { + const previous = Number(lastByStream.get(stream)); + if (current !== previous + 1) { + gaps.push(Object.freeze({ + stream_id: stream, + previous, + expected: previous + 1, + actual: current, + kind: current <= previous ? "regression" : "gap", + event_id: event.event_id, + })); + } + } + lastByStream.set(stream, current); + } + + return Object.freeze({ + events: Object.freeze(events), + duplicate_event_ids: Object.freeze(duplicates), + gaps: Object.freeze(gaps), + reconciliation_required: gaps.length > 0, + stream_positions: Object.freeze( + Object.fromEntries([...lastByStream.entries()].map(([key, value]) => [key, Number(value)])), + ), + }); +} + +function createTimelinePage(input = {}) { + const analysis = analyzeCortexTimeline(input.events || [], { + initial_sequences: input.initial_sequences || {}, + }); + return Object.freeze({ + events: analysis.events, + cursor: normalizeTimelineCursor(input.cursor), + next_cursor: normalizeTimelineCursor(input.next_cursor), + duplicate_event_ids: analysis.duplicate_event_ids, + gaps: analysis.gaps, + reconciliation: Object.freeze({ + required: analysis.reconciliation_required, + reason: analysis.reconciliation_required ? "sequence_gap_or_regression" : null, + }), + stream_positions: analysis.stream_positions, + }); +} + +module.exports = { + TIMELINE_CURSOR_SCHEMA_VERSION, + normalizeTimelineCursor, + analyzeCortexTimeline, + createTimelinePage, +}; From f44c62f484dcab02a86a483ce1b0f9047dfb1799 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:30:35 +0800 Subject: [PATCH 088/166] feat(m040): export CortexEvent timeline contracts --- packages/protocol/src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/protocol/src/index.js b/packages/protocol/src/index.js index ce08a7e0..3153e300 100644 --- a/packages/protocol/src/index.js +++ b/packages/protocol/src/index.js @@ -5,5 +5,7 @@ module.exports = { ...require("./version"), ...require("./result"), ...require("./capabilities"), + ...require("./events"), + ...require("./timeline"), ...require("./negotiation"), }; From bd20fc48216a4c57172c1135d8f0254eac6bea1c Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:30:38 +0800 Subject: [PATCH 089/166] test(m040): cover CortexEvent timeline contract --- tests/protocol/events.test.js | 105 ++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/protocol/events.test.js diff --git a/tests/protocol/events.test.js b/tests/protocol/events.test.js new file mode 100644 index 00000000..45e4a034 --- /dev/null +++ b/tests/protocol/events.test.js @@ -0,0 +1,105 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const protocol = require(path.join(ROOT, "packages", "protocol", "src")); + +function event(overrides = {}) { + return { + event_id: "coordination:CE-1", + type: "coordination.task.progress", + occurred_at: "2026-09-29T04:30:00.000Z", + source: { + kind: "coordination", + source_event_id: "CE-1", + producer_id: "agent-1", + producer_kind: "agent", + project_ref: "project:cortex-agent", + }, + correlation: { + task_id: "T-1", + run_ref: "run:R-1", + }, + sequence: { + stream_id: "coordination:T-1:agent-1", + value: 1, + }, + payload: { state: "EXECUTING" }, + evidence_refs: [], + redacted: true, + ...overrides, + }; +} + +test("CortexEvent v1 is a closed portable envelope", () => { + const value = protocol.normalizeCortexEvent(event()); + assert.equal(value.schema_version, "1"); + assert.equal(value.source.project_ref, "project:cortex-agent"); + assert.equal(value.correlation.run_ref, "run:R-1"); + assert.equal(value.sequence.value, 1); + assert.equal(Object.isFrozen(value), true); +}); + +test("CortexEvent rejects invalid canonical refs and unknown fields", () => { + assert.throws( + () => protocol.normalizeCortexEvent(event({ + source: { + kind: "coordination", + source_event_id: "CE-1", + project_ref: "runtime:not-a-project", + }, + })), + (error) => error.code === "ERR_CORTEX_EVENT_REF", + ); + assert.throws( + () => protocol.normalizeCortexEvent(event({ vendor_magic: true })), + (error) => error.code === "ERR_CORTEX_EVENT_FIELD_UNKNOWN", + ); +}); + +test("timeline dedupes by event identity and detects source-local sequence gaps", () => { + const first = event(); + const duplicate = event(); + const third = event({ + event_id: "coordination:CE-3", + source: { ...event().source, source_event_id: "CE-3" }, + sequence: { + stream_id: "coordination:T-1:agent-1", + value: 3, + }, + }); + const analysis = protocol.analyzeCortexTimeline([first, duplicate, third]); + assert.equal(analysis.events.length, 2); + assert.deepEqual(analysis.duplicate_event_ids, ["coordination:CE-1"]); + assert.equal(analysis.gaps.length, 1); + assert.equal(analysis.gaps[0].expected, 2); + assert.equal(analysis.gaps[0].actual, 3); + assert.equal(analysis.reconciliation_required, true); +}); + +test("first event in a partial stream establishes baseline unless initial sequence is supplied", () => { + const third = event({ + event_id: "coordination:CE-3", + source: { ...event().source, source_event_id: "CE-3" }, + sequence: { stream_id: "coordination:T-1:agent-1", value: 3 }, + }); + assert.equal(protocol.analyzeCortexTimeline([third]).gaps.length, 0); + const withCursor = protocol.analyzeCortexTimeline([third], { + initial_sequences: { "coordination:T-1:agent-1": 1 }, + }); + assert.equal(withCursor.gaps.length, 1); + assert.equal(withCursor.gaps[0].expected, 2); +}); + +test("timeline cursor position is opaque and not confused with event sequence", () => { + const cursor = protocol.normalizeTimelineCursor({ + stream_id: "event-bus:test", + position: "byte:4096", + event_id: "framework:eb-evt-1", + }); + assert.equal(cursor.position, "byte:4096"); + assert.equal(Object.prototype.hasOwnProperty.call(cursor, "sequence"), false); +}); From ffb7b262240bd5ee164f5a8362d1763ea7fddcf2 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:30:41 +0800 Subject: [PATCH 090/166] test(m040): include CortexEvent protocol tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75af439f..4ca81801 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js" }, "keywords": [ "ai", From e14edc1745d1eaa18cc050bf6b0b8ff91040aa66 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:30:44 +0800 Subject: [PATCH 091/166] ci(m040): validate event bridge changes --- .github/workflows/m040-validation.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml index 303ea9b0..8d4572ec 100644 --- a/.github/workflows/m040-validation.yml +++ b/.github/workflows/m040-validation.yml @@ -12,12 +12,14 @@ on: - "lib/sdk/**" - "lib/runtime-port/**" - "lib/control-service/**" + - "lib/event-bridge/**" - "lib/commands/management/query.js" - "tests/architecture/**" - "tests/protocol/**" - "tests/sdk/**" - "tests/runtime-port/**" - "tests/control-service/**" + - "tests/event-bridge/**" - "tests/management/management-query-cli.test.js" - ".github/workflows/m040-validation.yml" pull_request: From 536500801647f00ccb9a0f0219ffaf5acd53e9d0 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:32:14 +0800 Subject: [PATCH 092/166] feat(m040): normalize Framework events to CortexEvent --- lib/event-bridge/framework-event-bus.js | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 lib/event-bridge/framework-event-bus.js diff --git a/lib/event-bridge/framework-event-bus.js b/lib/event-bridge/framework-event-bus.js new file mode 100644 index 00000000..e8c00aa4 --- /dev/null +++ b/lib/event-bridge/framework-event-bus.js @@ -0,0 +1,94 @@ +"use strict"; + +const { + canonicalEventId, + createRef, + normalizeCortexEvent, +} = require("../../packages/protocol/src/index.js"); +const { validateEvent } = require("../event-bus/event-types.js"); + +const TYPE_MAP = Object.freeze({ + subagent_spawned: "agent.subagent.spawned", + subagent_progress: "agent.subagent.progress", + subagent_completed: "agent.subagent.completed", + subagent_failed: "agent.subagent.failed", + subagent_cancelled: "agent.subagent.cancelled", + handoff_ready: "handoff.ready", + decision_resolved: "decision.resolved", + waitpoint_released: "waitpoint.released", +}); + +function customType(name) { + const raw = String(name || "").slice("custom:".length).toLowerCase(); + const body = raw + .replace(/[^a-z0-9-]+/g, ".") + .replace(/^\.+|\.+$/g, "") + .replace(/\.{2,}/g, "."); + return body ? `extension.custom.${body}` : "extension.custom.event"; +} + +function canonicalFrameworkType(name) { + if (TYPE_MAP[name]) return TYPE_MAP[name]; + if (typeof name === "string" && name.startsWith("custom:")) return customType(name); + const error = new Error(`Unsupported Framework Event Bus type: ${name}`); + error.code = "ERR_FRAMEWORK_EVENT_TYPE"; + throw error; +} + +function nonGlobal(value) { + return value && value !== "global" && value !== "host" ? value : null; +} + +function normalizeFrameworkEvent(event, options = {}) { + const validation = validateEvent(event); + if (!validation.valid) { + const error = new Error(`Invalid Framework Event Bus event: ${validation.errors.join("; ")}`); + error.code = "ERR_FRAMEWORK_EVENT_INVALID"; + error.details = { errors: validation.errors }; + throw error; + } + + const parentRunId = nonGlobal(event.correlation.parent_run_id); + const sessionId = event.producer.session_id || null; + const sequence = Number.isSafeInteger(options.sequence) + ? { + stream_id: options.stream_id || `framework:${event.bus_id}`, + value: options.sequence, + } + : null; + + return normalizeCortexEvent({ + event_id: canonicalEventId("framework", event.event_id), + type: canonicalFrameworkType(event.event_name), + occurred_at: event.occurred_at, + source: { + kind: "framework", + source_event_id: event.event_id, + producer_id: event.producer.producer_id, + producer_kind: event.producer.producer_kind, + project_ref: options.project_ref || null, + host_ref: options.host_ref || null, + runtime_ref: options.runtime_ref || null, + }, + correlation: { + mission_id: nonGlobal(event.correlation.mission_id), + trace_id: nonGlobal(event.correlation.subagent_id), + run_ref: parentRunId ? createRef("run", parentRunId) : null, + session_ref: sessionId ? createRef("session", sessionId) : null, + correlation_id: options.correlation_id || null, + }, + sequence, + causation_id: event.correlation.causation_id + ? canonicalEventId("framework", event.correlation.causation_id) + : null, + payload: { ...event.payload }, + evidence_refs: options.evidence_refs || [], + redacted: options.redacted === true, + }); +} + +module.exports = { + TYPE_MAP, + canonicalFrameworkType, + normalizeFrameworkEvent, +}; From 1e79329b868f3584883bcce3b5bbc73c85bea275 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:32:17 +0800 Subject: [PATCH 093/166] feat(m040): normalize Coordination events to CortexEvent --- lib/event-bridge/coordination.js | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/event-bridge/coordination.js diff --git a/lib/event-bridge/coordination.js b/lib/event-bridge/coordination.js new file mode 100644 index 00000000..4c564da6 --- /dev/null +++ b/lib/event-bridge/coordination.js @@ -0,0 +1,61 @@ +"use strict"; + +const { + canonicalEventId, + createRef, + normalizeCortexEvent, +} = require("../../packages/protocol/src/index.js"); +const { validateEvent } = require("../coordination/contract.js"); + +function normalizeCoordinationEvent(event, options = {}) { + validateEvent(event); + + const sessionId = event.producer && event.producer.sessionId + ? event.producer.sessionId + : null; + const evidenceRefs = Array.isArray(event.evidence) + ? event.evidence.map((item) => item.ref).filter(Boolean) + : []; + + return normalizeCortexEvent({ + event_id: canonicalEventId("coordination", event.eventId), + type: `coordination.${event.eventType}`, + occurred_at: event.timestamp, + source: { + kind: "coordination", + source_event_id: event.eventId, + producer_id: event.producer.actorId, + producer_kind: event.producer.kind, + project_ref: createRef("project", event.projectId), + host_ref: options.host_ref || null, + runtime_ref: options.runtime_ref || null, + }, + correlation: { + mission_id: options.mission_id || null, + milestone_id: options.milestone_id || null, + task_id: event.taskId, + operation_id: event.operationId || null, + correlation_id: event.correlationId, + run_ref: options.run_ref || null, + session_ref: sessionId ? createRef("session", sessionId) : null, + workspace_ref: options.workspace_ref || null, + }, + sequence: { + stream_id: `coordination:${event.taskId}:${event.producer.actorId}`, + value: event.sequence, + }, + causation_id: options.causation_id || null, + payload: { + previous_state: event.previousState, + current_state: event.currentState, + operation_attempt: event.operationAttempt || null, + parent_task_id: event.parentTaskId || null, + }, + evidence_refs: evidenceRefs, + redacted: true, + }); +} + +module.exports = { + normalizeCoordinationEvent, +}; From 65027566b80be1343c7c177cc7932d09f6261fd5 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:32:19 +0800 Subject: [PATCH 094/166] feat(m040): normalize Runtime boundary events --- lib/event-bridge/runtime-boundary.js | 63 ++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/event-bridge/runtime-boundary.js diff --git a/lib/event-bridge/runtime-boundary.js b/lib/event-bridge/runtime-boundary.js new file mode 100644 index 00000000..be61382a --- /dev/null +++ b/lib/event-bridge/runtime-boundary.js @@ -0,0 +1,63 @@ +"use strict"; + +const { + canonicalEventId, + createRef, + normalizeCortexEvent, +} = require("../../packages/protocol/src/index.js"); +const { + validateBoundaryEvent, +} = require("../runtime-adapters/boundary-event.js"); + +function normalizeRuntimeBoundaryEvent(input, options = {}) { + const event = validateBoundaryEvent(input); + const correlation = event.correlation || {}; + const runtimeRef = options.runtime_ref + || createRef("runtime", `native:${event.host.adapter_id}`); + + return normalizeCortexEvent({ + event_id: canonicalEventId("runtime", event.event_id), + type: `runtime.${event.type}`, + occurred_at: event.at, + source: { + kind: "runtime", + source_event_id: event.event_id, + producer_id: event.host.adapter_id, + producer_kind: "runtime-adapter", + project_ref: options.project_ref || null, + host_ref: options.host_ref || null, + runtime_ref: runtimeRef, + }, + correlation: { + mission_id: options.mission_id || null, + milestone_id: options.milestone_id || null, + task_id: correlation.task_id || null, + operation_id: correlation.operation_id || null, + trace_id: correlation.trace_id || null, + correlation_id: options.correlation_id || null, + run_ref: correlation.run_id ? createRef("run", correlation.run_id) : null, + session_ref: correlation.session_id + ? createRef("session", correlation.session_id) + : null, + workspace_ref: options.workspace_ref || null, + }, + sequence: Number.isSafeInteger(options.sequence) + ? { + stream_id: options.stream_id || `runtime:${runtimeRef}`, + value: options.sequence, + } + : null, + causation_id: options.causation_id || null, + payload: { + resource: event.resource, + capability: event.capability, + decision: event.decision, + }, + evidence_refs: event.evidence_refs || [], + redacted: true, + }); +} + +module.exports = { + normalizeRuntimeBoundaryEvent, +}; From b8b96917707cbbaff296c371bca5c4a21dc5c138 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:32:22 +0800 Subject: [PATCH 095/166] test(m040): cover CortexEvent source bridges --- tests/event-bridge/event-bridge.test.js | 158 ++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/event-bridge/event-bridge.test.js diff --git a/tests/event-bridge/event-bridge.test.js b/tests/event-bridge/event-bridge.test.js new file mode 100644 index 00000000..b30ba93d --- /dev/null +++ b/tests/event-bridge/event-bridge.test.js @@ -0,0 +1,158 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + normalizeFrameworkEvent, +} = require(path.join(ROOT, "lib", "event-bridge", "framework-event-bus.js")); +const { + normalizeCoordinationEvent, +} = require(path.join(ROOT, "lib", "event-bridge", "coordination.js")); +const { + normalizeRuntimeBoundaryEvent, +} = require(path.join(ROOT, "lib", "event-bridge", "runtime-boundary.js")); + +function frameworkEvent() { + return { + event_id: "eb-evt-00000001-0000-0000-0000-000000000000", + event_name: "subagent_completed", + event_version: "1.0", + bus_id: "test-host:m-040", + occurred_at: "2026-09-29T04:30:00.000Z", + producer: { + producer_id: "sub-1", + producer_kind: "sub_agent", + session_id: "S-1", + }, + correlation: { + mission_id: "M-040", + subagent_id: "sub-1", + parent_run_id: "R-1", + causation_id: null, + }, + payload: { + status: "success", + output_summary: "done", + }, + }; +} + +test("Framework Event Bus projects into CortexEvent without inventing sequence", () => { + const value = normalizeFrameworkEvent(frameworkEvent(), { + project_ref: "project:cortex-agent", + }); + assert.equal(value.event_id.startsWith("framework:"), true); + assert.equal(value.type, "agent.subagent.completed"); + assert.equal(value.sequence, null); + assert.equal(value.correlation.run_ref, "run:R-1"); + assert.equal(value.redacted, false); +}); + +test("Framework sequence is only present when an explicit source-local ordinal is supplied", () => { + const value = normalizeFrameworkEvent(frameworkEvent(), { + sequence: 9, + stream_id: "framework:test-host:m-040", + }); + assert.deepEqual(value.sequence, { + stream_id: "framework:test-host:m-040", + value: 9, + }); +}); + +function coordinationEvent() { + return { + schemaVersion: "1.0", + eventId: "CE-20260929-001", + projectId: "cortex-agent", + taskId: "T-1", + parentTaskId: null, + correlationId: "corr-1", + producer: { + actorId: "agent-1", + kind: "agent", + sessionId: "S-1", + }, + targets: [], + eventType: "task.created", + previousState: null, + currentState: "CREATED", + timestamp: "2026-09-29T04:30:00.000Z", + sequence: 1, + repository: { repositoryId: "cortex-agent" }, + fileOwnership: [], + progress: null, + message: null, + evidence: [], + requestedAction: null, + expiresAt: null, + notification: { policy: "journal_only", dedupeKey: "task.created" }, + operationId: null, + operationAttempt: null, + }; +} + +test("Coordination journal preserves its strict source-local sequence in CortexEvent", () => { + const value = normalizeCoordinationEvent(coordinationEvent(), { + mission_id: "M-040", + }); + assert.equal(value.type, "coordination.task.created"); + assert.deepEqual(value.sequence, { + stream_id: "coordination:T-1:agent-1", + value: 1, + }); + assert.equal(value.source.project_ref, "project:cortex-agent"); + assert.equal(value.correlation.session_ref, "session:S-1"); + assert.equal(value.redacted, true); +}); + +function runtimeEvent() { + return { + schema_version: "1.0", + event_id: "RBE-1", + type: "tool.before", + at: "2026-09-29T04:30:00.000Z", + host: { + adapter_id: "codex", + session_ref: "host-session-1", + }, + correlation: { + task_id: "T-1", + run_id: "R-1", + session_id: "S-1", + operation_id: "OP-1", + trace_id: "TRACE-1", + }, + resource: { + kind: "tool", + name: "dispatch.execute", + }, + capability: "tool.before.block", + decision: { + result: "allowed", + authorization_ref: "D-1", + }, + evidence_refs: ["decision:D-1"], + }; +} + +test("Runtime Boundary projection keeps adapter identity below RuntimeRef and does not invent physical HostRef", () => { + const value = normalizeRuntimeBoundaryEvent(runtimeEvent()); + assert.equal(value.type, "runtime.tool.before"); + assert.equal(value.source.runtime_ref, "runtime:native:codex"); + assert.equal(value.source.host_ref, null); + assert.equal(value.correlation.run_ref, "run:R-1"); + assert.equal(value.redacted, true); +}); + +test("Runtime Boundary may attach an explicit physical HostRef without changing adapter identity", () => { + const value = normalizeRuntimeBoundaryEvent(runtimeEvent(), { + host_ref: "host:mac-mini", + runtime_ref: "runtime:paseo:mac-mini", + }); + assert.equal(value.source.host_ref, "host:mac-mini"); + assert.equal(value.source.runtime_ref, "runtime:paseo:mac-mini"); + assert.equal(value.source.producer_id, "codex"); +}); From 3e731668cbcf1a928c7d600b8b9e4dd160b96a01 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:32:25 +0800 Subject: [PATCH 096/166] test(m040): include CortexEvent bridge tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ca81801..5c53838a 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js" }, "keywords": [ "ai", From 31ade07c8f637bdd00839b1d892c4d96873f0bff Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:33:35 +0800 Subject: [PATCH 097/166] docs: define M-040 CortexEvent timeline --- docs/architecture/cortex-event-timeline.md | 191 +++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/architecture/cortex-event-timeline.md diff --git a/docs/architecture/cortex-event-timeline.md b/docs/architecture/cortex-event-timeline.md new file mode 100644 index 00000000..e073320e --- /dev/null +++ b/docs/architecture/cortex-event-timeline.md @@ -0,0 +1,191 @@ +# CortexEvent v1 and Timeline Contract + +> **Status**: M-040 MS-007 baseline +> **Validated**: 2026-09-29 +> **Focused CI**: 78/78 PASS + +## 1. Position + +CortexEvent is a canonical **observation envelope** across Cortex domains. + +It does not replace the authoritative journals or state stores that produced the event. + +```text +Coordination journal ─┐ +Framework Event Bus ──┼─> source bridge -> CortexEvent v1 -> consumers +Runtime Boundary ─────┘ +``` + +## 2. Envelope + +CortexEvent v1 contains: + +- stable canonical event_id; +- dotted event type; +- occurred_at; +- source identity; +- correlation identity; +- optional source-local sequence; +- optional causation id; +- bounded/projected payload; +- evidence refs; +- explicit redaction flag. + +The contract lives in `@cortex-agent/protocol`. + +## 3. Source identity + +Source kinds include: + +- framework +- coordination +- runtime +- project +- extension +- control + +Source identity may carry canonical ProjectRef, HostRef and RuntimeRef. + +A coding-agent adapter id is not automatically a physical HostRef. + +## 4. Correlation + +Canonical correlation supports: + +- mission_id +- milestone_id +- task_id +- decision_id +- waitpoint_id +- operation_id +- trace_id +- correlation_id +- RunRef +- SessionRef +- WorkspaceRef + +Correlation does not become state ownership. + +## 5. Sequence and ordering + +There is no fake global sequence. + +```text +Coordination journal + -> strict producer/task sequence + -> preserved in CortexEvent + +Framework Event Bus + -> no intrinsic event sequence + -> no CortexEvent sequence invented + -> replay cursor remains separate +``` + +If a source supplies an explicit local ordinal, it may be represented as: + +```json +{ + "stream_id": "coordination:T-1:agent-1", + "value": 42 +} +``` + +## 6. Cursor + +Timeline cursor position is opaque. + +Examples: + +```text +byte:4096 +event:123 +page:abc +``` + +A cursor is not treated as an event sequence. + +This allows existing Event Bus byte offsets and future remote cursors to coexist without inventing common ordering semantics. + +## 7. Replay, dedupe and gaps + +Timeline analysis: + +1. normalizes events; +2. deduplicates by canonical event_id; +3. tracks each source-local stream independently; +4. detects gaps and regressions; +5. marks reconciliation as required when continuity breaks. + +A partial timeline does not assume its first observed sequence must be 1. A caller may provide the previous known sequence when continuing from a cursor. + +## 8. Reconciliation + +When a gap/regression is found: + +```text +reconciliation.required = true +reason = sequence_gap_or_regression +``` + +Consumers must query authoritative state/snapshot owners instead of guessing missing state from the event stream. + +## 9. Source bridges + +### Framework Event Bus + +Maps existing bus event IDs/types/correlation into CortexEvent. + +Framework payloads are not assumed redacted unless the caller explicitly knows they are. + +No sequence is invented from byte offsets. + +### Coordination + +The existing Coordination contract validates the source event first. + +Strict per-task/producer sequence is preserved. + +The CortexEvent projection deliberately omits free-form message content and keeps state/evidence relations. + +### Runtime Boundary + +The existing Runtime Boundary validator runs first, including taint/secret checks. + +The projection is therefore marked redacted-safe. + +RuntimeRef may be derived from the native adapter or explicitly supplied by a higher runtime layer such as Paseo. + +Physical HostRef is attached only when explicitly known. + +## 10. Authority + +```text +CortexEvent != command +CortexEvent != authorization +CortexEvent != source-of-truth state +``` + +Event consumers that want to mutate governed state must call an explicit governed command/owner API. + +## 11. Validation + +Focused tests cover: + +- closed event envelope; +- canonical ref validation; +- dedupe; +- gap/regression detection; +- partial-stream baseline; +- opaque cursor semantics; +- Framework Event Bus projection; +- Coordination strict sequence projection; +- Runtime Boundary projection and redaction; +- physical HostRef not inferred from adapter identity. + +Result: + +```text +78 tests +78 pass +0 fail +``` From 4d0b4657bb5967167db6df94497b8c83abe3eaa1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:33:38 +0800 Subject: [PATCH 098/166] docs: index CortexEvent architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index c7e0d870..315fc63e 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -32,6 +32,7 @@ - [RuntimePort v1 and Native Adapter Compatibility](./runtime-port-v1.md) - [Host / Runtime Topology and Capability Routing](./runtime-topology-routing.md) - [Control Service and Daemon Boundary](./control-service-boundary.md) +- [CortexEvent v1 and Timeline Contract](./cortex-event-timeline.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From f4eff92392c4dee9b3427d510fa62f22d3247f77 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:02 +0800 Subject: [PATCH 099/166] feat(m040): add extension-sdk package --- packages/extension-sdk/package.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/extension-sdk/package.json diff --git a/packages/extension-sdk/package.json b/packages/extension-sdk/package.json new file mode 100644 index 00000000..76ee0c8c --- /dev/null +++ b/packages/extension-sdk/package.json @@ -0,0 +1,17 @@ +{ + "name": "@cortex-agent/extension-sdk", + "version": "0.0.0", + "private": true, + "description": "Governed extension manifest and permission contracts for Cortex Agent", + "main": "src/index.js", + "exports": { + ".": "./src/index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "license": "MIT", + "dependencies": { + "@cortex-agent/protocol": "workspace:*" + } +} From d6dfe59751f8acc6d53075a0cdeba6a7f03568c3 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:05 +0800 Subject: [PATCH 100/166] feat(m040): define extension manifest contract --- packages/extension-sdk/src/manifest.js | 225 +++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 packages/extension-sdk/src/manifest.js diff --git a/packages/extension-sdk/src/manifest.js b/packages/extension-sdk/src/manifest.js new file mode 100644 index 00000000..43482a2f --- /dev/null +++ b/packages/extension-sdk/src/manifest.js @@ -0,0 +1,225 @@ +"use strict"; + +let protocol; +try { + protocol = require("@cortex-agent/protocol"); +} catch (_) { + protocol = require("../../protocol/src/index.js"); +} + +const EXTENSION_MANIFEST_SCHEMA_VERSION = "1"; +const EXTENSION_TYPES = Object.freeze([ + "agent-adapter", + "host-adapter", + "runtime-adapter", + "knowledge-provider", + "policy-provider", + "workflow", + "skill", + "validator", + "event-consumer", + "ui-surface", + "project-adapter", + "project-provider", +]); + +const ENTRY_POINT_KEYS = Object.freeze(["server", "client", "cli", "worker"]); +const TOP_KEYS = new Set([ + "schema_version", + "id", + "version", + "type", + "cortex", + "capabilities", + "permissions", + "entry_points", + "config_schema", +]); +const CORTEX_KEYS = new Set([ + "protocol", + "min_version", + "max_version_exclusive", +]); +const CAPABILITY_KEYS = new Set(["provided", "required"]); + +class ExtensionManifestError extends Error { + constructor(code, details = {}) { + super(`[extension-manifest:${code}] ${JSON.stringify(details)}`); + this.name = "ExtensionManifestError"; + this.code = code; + this.details = details; + } +} + +function plain(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function rejectUnknown(value, known, where) { + for (const key of Object.keys(value || {})) { + if (!known.has(key)) { + throw new ExtensionManifestError("ERR_EXTENSION_FIELD_UNKNOWN", { where, key }); + } + } +} + +function string(value, where) { + if (typeof value !== "string" || !value.trim()) { + throw new ExtensionManifestError("ERR_EXTENSION_FIELD_INVALID", { where }); + } + return value.trim(); +} + +function normalizeId(value) { + const id = string(value, "id"); + if (!/^[a-z0-9][a-z0-9._-]{1,127}$/.test(id)) { + throw new ExtensionManifestError("ERR_EXTENSION_ID_INVALID", { id }); + } + return id; +} + +function normalizeVersion(value, where) { + const version = string(value, where); + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { + throw new ExtensionManifestError("ERR_EXTENSION_VERSION_INVALID", { where, version }); + } + return version; +} + +function normalizeCortexCompatibility(input) { + if (!plain(input)) { + throw new ExtensionManifestError("ERR_EXTENSION_CORTEX_COMPATIBILITY", {}); + } + rejectUnknown(input, CORTEX_KEYS, "cortex"); + const protocolName = string(input.protocol, "cortex.protocol"); + if (protocolName !== protocol.EXTENSION_PROTOCOL_NAME) { + throw new ExtensionManifestError("ERR_EXTENSION_PROTOCOL", { + expected: protocol.EXTENSION_PROTOCOL_NAME, + received: protocolName, + }); + } + const min = string(input.min_version, "cortex.min_version"); + const max = string(input.max_version_exclusive, "cortex.max_version_exclusive"); + if (!protocol.parseProtocolVersion(min) || !protocol.parseProtocolVersion(max)) { + throw new ExtensionManifestError("ERR_EXTENSION_PROTOCOL_VERSION", { min, max }); + } + if (protocol.compareProtocolVersions(min, max) >= 0) { + throw new ExtensionManifestError("ERR_EXTENSION_COMPATIBILITY_RANGE", { min, max }); + } + return Object.freeze({ + protocol: protocolName, + min_version: min, + max_version_exclusive: max, + }); +} + +function isCortexVersionCompatible(compatibility, version = protocol.EXTENSION_PROTOCOL_VERSION) { + const current = protocol.parseProtocolVersion(version); + const min = protocol.parseProtocolVersion(compatibility.min_version); + const max = protocol.parseProtocolVersion(compatibility.max_version_exclusive); + if (!current || !min || !max) return false; + return current.major === min.major + && current.major === max.major - (max.minor === 0 ? 1 : 0) + ? protocol.compareProtocolVersions(current, min) >= 0 + && protocol.compareProtocolVersions(current, max) < 0 + : protocol.compareProtocolVersions(current, min) >= 0 + && protocol.compareProtocolVersions(current, max) < 0; +} + +function normalizeCapabilities(input) { + if (input == null) input = {}; + if (!plain(input)) { + throw new ExtensionManifestError("ERR_EXTENSION_CAPABILITIES", {}); + } + rejectUnknown(input, CAPABILITY_KEYS, "capabilities"); + return Object.freeze({ + provided: protocol.validateCapabilityList(input.provided || []), + required: protocol.validateCapabilityList(input.required || []), + }); +} + +function normalizeEntryPoint(value, key) { + const entry = string(value, `entry_points.${key}`); + if (entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry)) { + throw new ExtensionManifestError("ERR_EXTENSION_ENTRY_POINT_ABSOLUTE", { key, entry }); + } + const normalized = entry.replace(/\\/g, "/"); + if (normalized.split("/").includes("..")) { + throw new ExtensionManifestError("ERR_EXTENSION_ENTRY_POINT_TRAVERSAL", { key, entry }); + } + return normalized; +} + +function normalizeEntryPoints(input) { + if (input == null) return Object.freeze({}); + if (!plain(input)) { + throw new ExtensionManifestError("ERR_EXTENSION_ENTRY_POINTS", {}); + } + const allowed = new Set(ENTRY_POINT_KEYS); + rejectUnknown(input, allowed, "entry_points"); + const out = {}; + for (const key of ENTRY_POINT_KEYS) { + if (input[key] != null) out[key] = normalizeEntryPoint(input[key], key); + } + return Object.freeze(out); +} + +function normalizeExtensionManifest(input, permissionNormalizer) { + if (!plain(input)) { + throw new ExtensionManifestError("ERR_EXTENSION_MANIFEST_INVALID", {}); + } + rejectUnknown(input, TOP_KEYS, "manifest"); + + const schemaVersion = input.schema_version == null + ? EXTENSION_MANIFEST_SCHEMA_VERSION + : String(input.schema_version); + if (schemaVersion !== EXTENSION_MANIFEST_SCHEMA_VERSION) { + throw new ExtensionManifestError("ERR_EXTENSION_SCHEMA_VERSION", { + received: schemaVersion, + }); + } + + const type = string(input.type, "type"); + if (!EXTENSION_TYPES.includes(type)) { + throw new ExtensionManifestError("ERR_EXTENSION_TYPE_UNKNOWN", { type }); + } + + const compatibility = normalizeCortexCompatibility(input.cortex); + if (!isCortexVersionCompatible(compatibility)) { + throw new ExtensionManifestError("ERR_EXTENSION_CORTEX_INCOMPATIBLE", { + current: protocol.EXTENSION_PROTOCOL_VERSION, + compatibility, + }); + } + + if (typeof permissionNormalizer !== "function") { + throw new ExtensionManifestError("ERR_EXTENSION_PERMISSION_NORMALIZER_REQUIRED", {}); + } + + const configSchema = input.config_schema == null ? null : input.config_schema; + if (configSchema !== null && !plain(configSchema)) { + throw new ExtensionManifestError("ERR_EXTENSION_CONFIG_SCHEMA", {}); + } + + return Object.freeze({ + schema_version: EXTENSION_MANIFEST_SCHEMA_VERSION, + id: normalizeId(input.id), + version: normalizeVersion(input.version, "version"), + type, + cortex: compatibility, + capabilities: normalizeCapabilities(input.capabilities), + permissions: permissionNormalizer(input.permissions || {}), + entry_points: normalizeEntryPoints(input.entry_points), + config_schema: configSchema === null ? null : Object.freeze({ ...configSchema }), + }); +} + +module.exports = { + EXTENSION_MANIFEST_SCHEMA_VERSION, + EXTENSION_TYPES, + ENTRY_POINT_KEYS, + ExtensionManifestError, + normalizeCortexCompatibility, + isCortexVersionCompatible, + normalizeExtensionManifest, +}; From 250f5f54c486b2d118384a86507e0efe1db0f4f1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:07 +0800 Subject: [PATCH 101/166] feat(m040): define extension permission policy --- packages/extension-sdk/src/permissions.js | 236 ++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 packages/extension-sdk/src/permissions.js diff --git a/packages/extension-sdk/src/permissions.js b/packages/extension-sdk/src/permissions.js new file mode 100644 index 00000000..ad3956ed --- /dev/null +++ b/packages/extension-sdk/src/permissions.js @@ -0,0 +1,236 @@ +"use strict"; + +const PERMISSION_OUTCOMES = Object.freeze([ + "allow", + "deny", + "approval_required", +]); + +const PERMISSION_IDS = Object.freeze([ + "filesystem.read", + "filesystem.write", + "git.read", + "git.commit", + "git.push", + "process.spawn", + "network.connect", + "secrets.read", +]); + +const ROOT_KEYS = new Set(["filesystem", "git", "process", "network", "secrets"]); +const FS_KEYS = new Set(["read", "write"]); +const GIT_KEYS = new Set(["read", "commit", "push"]); +const PROCESS_KEYS = new Set(["spawn"]); +const NETWORK_KEYS = new Set(["allow"]); +const SECRET_KEYS = new Set(["read"]); + +class ExtensionPermissionError extends Error { + constructor(code, details = {}) { + super(`[extension-permission:${code}] ${JSON.stringify(details)}`); + this.name = "ExtensionPermissionError"; + this.code = code; + this.details = details; + } +} + +function plain(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function rejectUnknown(value, known, where) { + for (const key of Object.keys(value || {})) { + if (!known.has(key)) { + throw new ExtensionPermissionError("ERR_PERMISSION_FIELD_UNKNOWN", { where, key }); + } + } +} + +function normalizeStringList(value, where) { + if (value == null) return Object.freeze([]); + if (!Array.isArray(value)) { + throw new ExtensionPermissionError("ERR_PERMISSION_SCOPE_LIST", { where }); + } + const out = []; + const seen = new Set(); + for (const raw of value) { + if (typeof raw !== "string" || !raw.trim() || /[\r\n]/.test(raw)) { + throw new ExtensionPermissionError("ERR_PERMISSION_SCOPE", { where, value: raw }); + } + const item = raw.trim(); + if (!seen.has(item)) { + seen.add(item); + out.push(item); + } + } + return Object.freeze(out); +} + +function bool(value, where) { + if (value == null) return false; + if (typeof value !== "boolean") { + throw new ExtensionPermissionError("ERR_PERMISSION_BOOLEAN", { where }); + } + return value; +} + +function objectSection(value, keys, where) { + if (value == null) return {}; + if (!plain(value)) { + throw new ExtensionPermissionError("ERR_PERMISSION_SECTION", { where }); + } + rejectUnknown(value, keys, where); + return value; +} + +function normalizePermissions(input) { + if (input == null) input = {}; + if (!plain(input)) { + throw new ExtensionPermissionError("ERR_PERMISSIONS_INVALID", {}); + } + rejectUnknown(input, ROOT_KEYS, "permissions"); + + const fs = objectSection(input.filesystem, FS_KEYS, "permissions.filesystem"); + const git = objectSection(input.git, GIT_KEYS, "permissions.git"); + const process = objectSection(input.process, PROCESS_KEYS, "permissions.process"); + const network = objectSection(input.network, NETWORK_KEYS, "permissions.network"); + const secrets = objectSection(input.secrets, SECRET_KEYS, "permissions.secrets"); + + return Object.freeze({ + filesystem: Object.freeze({ + read: normalizeStringList(fs.read, "permissions.filesystem.read"), + write: normalizeStringList(fs.write, "permissions.filesystem.write"), + }), + git: Object.freeze({ + read: bool(git.read, "permissions.git.read"), + commit: bool(git.commit, "permissions.git.commit"), + push: bool(git.push, "permissions.git.push"), + }), + process: Object.freeze({ + spawn: bool(process.spawn, "permissions.process.spawn"), + }), + network: Object.freeze({ + allow: normalizeStringList(network.allow, "permissions.network.allow"), + }), + secrets: Object.freeze({ + read: normalizeStringList(secrets.read, "permissions.secrets.read"), + }), + }); +} + +function flattenPermissionRequests(permissionsInput) { + const permissions = normalizePermissions(permissionsInput); + const out = []; + for (const scope of permissions.filesystem.read) { + out.push({ permission: "filesystem.read", scope }); + } + for (const scope of permissions.filesystem.write) { + out.push({ permission: "filesystem.write", scope }); + } + for (const id of ["read", "commit", "push"]) { + if (permissions.git[id]) out.push({ permission: `git.${id}`, scope: null }); + } + if (permissions.process.spawn) { + out.push({ permission: "process.spawn", scope: null }); + } + for (const scope of permissions.network.allow) { + out.push({ permission: "network.connect", scope }); + } + for (const scope of permissions.secrets.read) { + out.push({ permission: "secrets.read", scope }); + } + return Object.freeze(out.map((item) => Object.freeze(item))); +} + +function normalizePolicy(input) { + if (!plain(input)) { + throw new ExtensionPermissionError("ERR_PERMISSION_POLICY_INVALID", {}); + } + const allowed = new Set(["default", "rules"]); + rejectUnknown(input, allowed, "policy"); + const defaultEffect = input.default || "deny"; + if (!PERMISSION_OUTCOMES.includes(defaultEffect)) { + throw new ExtensionPermissionError("ERR_PERMISSION_OUTCOME", { effect: defaultEffect }); + } + if (!Array.isArray(input.rules || [])) { + throw new ExtensionPermissionError("ERR_PERMISSION_RULES", {}); + } + const rules = (input.rules || []).map((rule, index) => { + if (!plain(rule)) { + throw new ExtensionPermissionError("ERR_PERMISSION_RULE", { index }); + } + rejectUnknown(rule, new Set(["permission", "effect", "scopes"]), `policy.rules[${index}]`); + if (!PERMISSION_IDS.includes(rule.permission)) { + throw new ExtensionPermissionError("ERR_PERMISSION_ID", { + index, + permission: rule.permission, + }); + } + if (!PERMISSION_OUTCOMES.includes(rule.effect)) { + throw new ExtensionPermissionError("ERR_PERMISSION_OUTCOME", { + index, + effect: rule.effect, + }); + } + return Object.freeze({ + permission: rule.permission, + effect: rule.effect, + scopes: normalizeStringList(rule.scopes, `policy.rules[${index}].scopes`), + }); + }); + return Object.freeze({ + default: defaultEffect, + rules: Object.freeze(rules), + }); +} + +function ruleEffect(request, policy) { + const matching = policy.rules.filter((rule) => rule.permission === request.permission); + if (matching.length === 0) return policy.default; + + if (request.scope !== null) { + const exact = matching.find((rule) => rule.scopes.includes(request.scope)); + if (exact) return exact.effect; + const wildcard = matching.find((rule) => rule.scopes.includes("*")); + if (wildcard) return wildcard.effect; + const unscoped = matching.find((rule) => rule.scopes.length === 0); + return unscoped ? unscoped.effect : policy.default; + } + + const unscoped = matching.find((rule) => rule.scopes.length === 0); + return unscoped ? unscoped.effect : matching[0].effect; +} + +function evaluatePermissions(permissionsInput, policyInput) { + const requests = flattenPermissionRequests(permissionsInput); + const policy = normalizePolicy(policyInput); + const decisions = requests.map((request) => Object.freeze({ + ...request, + outcome: ruleEffect(request, policy), + })); + + let outcome = "allow"; + if (decisions.some((item) => item.outcome === "deny")) outcome = "deny"; + else if (decisions.some((item) => item.outcome === "approval_required")) { + outcome = "approval_required"; + } + + return Object.freeze({ + outcome, + requests, + decisions: Object.freeze(decisions), + enforcement: Object.freeze({ + claimed: false, + note: "policy evaluation is not a sandbox; enforcement belongs to the controlled boundary", + }), + }); +} + +module.exports = { + PERMISSION_OUTCOMES, + PERMISSION_IDS, + ExtensionPermissionError, + normalizePermissions, + flattenPermissionRequests, + normalizePolicy, + evaluatePermissions, +}; From cc93af2e404ccc309b64f07e4e7c6a88243782a1 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:10 +0800 Subject: [PATCH 102/166] feat(m040): add pure extension registry --- packages/extension-sdk/src/registry.js | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/extension-sdk/src/registry.js diff --git a/packages/extension-sdk/src/registry.js b/packages/extension-sdk/src/registry.js new file mode 100644 index 00000000..62313b5f --- /dev/null +++ b/packages/extension-sdk/src/registry.js @@ -0,0 +1,54 @@ +"use strict"; + +const { + normalizeExtensionManifest, + ExtensionManifestError, +} = require("./manifest"); +const { + normalizePermissions, +} = require("./permissions"); + +function createExtensionRegistry() { + const entries = new Map(); + + function register(input) { + const manifest = normalizeExtensionManifest(input, normalizePermissions); + const existing = entries.get(manifest.id); + if (existing) { + if (existing.version === manifest.version) { + return Object.freeze({ + registered: false, + idempotent: true, + manifest: existing, + }); + } + throw new ExtensionManifestError("ERR_EXTENSION_ID_CONFLICT", { + id: manifest.id, + existing_version: existing.version, + requested_version: manifest.version, + }); + } + entries.set(manifest.id, manifest); + return Object.freeze({ + registered: true, + idempotent: false, + manifest, + }); + } + + function get(id) { + return entries.get(id) || null; + } + + function list() { + return Object.freeze( + [...entries.values()].sort((a, b) => a.id.localeCompare(b.id)), + ); + } + + return Object.freeze({ register, get, list }); +} + +module.exports = { + createExtensionRegistry, +}; From b5e6aad36377cc69b3a936238f6e499beb443a5f Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:13 +0800 Subject: [PATCH 103/166] feat(m040): export extension SDK --- packages/extension-sdk/src/index.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/extension-sdk/src/index.js diff --git a/packages/extension-sdk/src/index.js b/packages/extension-sdk/src/index.js new file mode 100644 index 00000000..6eb6ce78 --- /dev/null +++ b/packages/extension-sdk/src/index.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + ...require("./manifest"), + ...require("./permissions"), + ...require("./registry"), +}; From f0e3684bf15b2ac62a83c270e4f09b38ed1a85ea Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:15 +0800 Subject: [PATCH 104/166] test(m040): cover extension permission architecture --- tests/extensions/extension-sdk.test.js | 147 +++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/extensions/extension-sdk.test.js diff --git a/tests/extensions/extension-sdk.test.js b/tests/extensions/extension-sdk.test.js new file mode 100644 index 00000000..c9a7032b --- /dev/null +++ b/tests/extensions/extension-sdk.test.js @@ -0,0 +1,147 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const ext = require(path.join(ROOT, "packages", "extension-sdk", "src")); + +function manifest(overrides = {}) { + return { + id: "runtime-paseo", + version: "0.1.0", + type: "runtime-adapter", + cortex: { + protocol: "cortex-extension", + min_version: "1.0", + max_version_exclusive: "2.0", + }, + capabilities: { + provided: ["runtime.run.create", "runtime.run.cancel"], + required: ["management.query"], + }, + permissions: { + filesystem: { + read: ["src/**"], + write: ["tests/**"], + }, + git: { + read: true, + commit: true, + push: false, + }, + process: { + spawn: true, + }, + network: { + allow: ["127.0.0.1"], + }, + secrets: { + read: [], + }, + }, + entry_points: { + server: "dist/server.js", + }, + config_schema: { + type: "object", + }, + ...overrides, + }; +} + +test("extension manifest freezes type, compatibility, capabilities and permissions", () => { + const value = ext.normalizeExtensionManifest(manifest(), ext.normalizePermissions); + assert.equal(value.type, "runtime-adapter"); + assert.deepEqual(value.capabilities.provided, [ + "runtime.run.create", + "runtime.run.cancel", + ]); + assert.equal(value.permissions.git.commit, true); + assert.equal(value.entry_points.server, "dist/server.js"); + assert.equal(Object.isFrozen(value), true); +}); + +test("unknown extension types and incompatible Cortex versions fail closed", () => { + assert.throws( + () => ext.normalizeExtensionManifest(manifest({ type: "magic-plugin" }), ext.normalizePermissions), + (error) => error.code === "ERR_EXTENSION_TYPE_UNKNOWN", + ); + assert.throws( + () => ext.normalizeExtensionManifest(manifest({ + cortex: { + protocol: "cortex-extension", + min_version: "2.0", + max_version_exclusive: "3.0", + }, + }), ext.normalizePermissions), + (error) => error.code === "ERR_EXTENSION_CORTEX_INCOMPATIBLE", + ); +}); + +test("extension entry points reject absolute and traversal paths", () => { + assert.throws( + () => ext.normalizeExtensionManifest(manifest({ + entry_points: { server: "../../escape.js" }, + }), ext.normalizePermissions), + (error) => error.code === "ERR_EXTENSION_ENTRY_POINT_TRAVERSAL", + ); + assert.throws( + () => ext.normalizeExtensionManifest(manifest({ + entry_points: { server: "/tmp/escape.js" }, + }), ext.normalizePermissions), + (error) => error.code === "ERR_EXTENSION_ENTRY_POINT_ABSOLUTE", + ); +}); + +test("permission policy is default-deny and deny overrides approval/allow", () => { + const evaluation = ext.evaluatePermissions(manifest().permissions, { + default: "deny", + rules: [ + { permission: "filesystem.read", effect: "allow", scopes: ["src/**"] }, + { permission: "filesystem.write", effect: "approval_required", scopes: ["tests/**"] }, + { permission: "git.read", effect: "allow" }, + { permission: "git.commit", effect: "approval_required" }, + { permission: "process.spawn", effect: "deny" }, + { permission: "network.connect", effect: "approval_required", scopes: ["127.0.0.1"] }, + ], + }); + + assert.equal(evaluation.outcome, "deny"); + assert.equal(evaluation.enforcement.claimed, false); + const write = evaluation.decisions.find((item) => item.permission === "filesystem.write"); + assert.equal(write.outcome, "approval_required"); + const spawn = evaluation.decisions.find((item) => item.permission === "process.spawn"); + assert.equal(spawn.outcome, "deny"); +}); + +test("approval_required is the aggregate outcome when no request is denied", () => { + const evaluation = ext.evaluatePermissions({ + git: { read: true, commit: true }, + }, { + default: "deny", + rules: [ + { permission: "git.read", effect: "allow" }, + { permission: "git.commit", effect: "approval_required" }, + ], + }); + assert.equal(evaluation.outcome, "approval_required"); +}); + +test("registry is deterministic, idempotent for same version and rejects version collision", () => { + const registry = ext.createExtensionRegistry(); + const first = registry.register(manifest()); + const second = registry.register(manifest()); + assert.equal(first.registered, true); + assert.equal(second.idempotent, true); + assert.equal(registry.list().length, 1); + assert.throws( + () => registry.register(manifest({ version: "0.2.0" })), + (error) => error.code === "ERR_EXTENSION_ID_CONFLICT", + ); +}); + +test("catalog plugin is not implicitly an executable extension type", () => { + assert.equal(ext.EXTENSION_TYPES.includes("plugin"), false); +}); From 36414bc1af50de1ac64dd2b38d256ab393a6f464 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:36:18 +0800 Subject: [PATCH 105/166] chore(m040): register extension-sdk workspace --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebc132fd..3131b429 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,3 +21,9 @@ importers: '@cortex-agent/protocol': specifier: workspace:* version: link:../protocol + + packages/extension-sdk: + dependencies: + '@cortex-agent/protocol': + specifier: workspace:* + version: link:../protocol From 362189886e9a3cbcc4312f30f20cf5d67ddfff45 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:57:40 +0800 Subject: [PATCH 106/166] test(m040): include extension SDK suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c53838a..4bc316e8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js tests/extensions/extension-sdk.test.js" }, "keywords": [ "ai", From b359599513040e7ff20f975b60d302a79c64cc08 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:57:43 +0800 Subject: [PATCH 107/166] ci(m040): include extension SDK validation --- .github/workflows/m040-validation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml index 8d4572ec..5d3eb5d5 100644 --- a/.github/workflows/m040-validation.yml +++ b/.github/workflows/m040-validation.yml @@ -20,6 +20,7 @@ on: - "tests/runtime-port/**" - "tests/control-service/**" - "tests/event-bridge/**" + - "tests/extensions/**" - "tests/management/management-query-cli.test.js" - ".github/workflows/m040-validation.yml" pull_request: From 54826681321c9457b18105e4cc0c72c2464ee213 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:57:46 +0800 Subject: [PATCH 108/166] docs: define M-040 extension permission architecture --- .../extension-permission-architecture.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/architecture/extension-permission-architecture.md diff --git a/docs/architecture/extension-permission-architecture.md b/docs/architecture/extension-permission-architecture.md new file mode 100644 index 00000000..35fa7233 --- /dev/null +++ b/docs/architecture/extension-permission-architecture.md @@ -0,0 +1,178 @@ +# Extension and Permission Architecture + +> **Status**: M-040 MS-008 baseline +> **Date**: 2026-09-29 + +## 1. Position + +Cortex extensions share a common governance contract without forcing existing Skills, catalog plugins, Agent Adapters, Host Adapters, UI bridges, or project integrations into one universal runtime container. + +The common contract covers: + +- identity; +- extension type; +- Cortex compatibility range; +- capabilities provided/required; +- permissions requested; +- entry points; +- configuration schema; +- deterministic registration. + +## 2. Extension types + +```text +agent-adapter +host-adapter +runtime-adapter +knowledge-provider +policy-provider +workflow +skill +validator +event-consumer +ui-surface +project-adapter +project-provider +``` + +A catalog `plugin` is intentionally not an executable extension type by default. Content/catalog objects only gain executable authority through an explicit adapter/manifest. + +## 3. Compatibility + +Every executable extension declares compatibility with the `cortex-extension` protocol: + +```yaml +cortex: + protocol: cortex-extension + min_version: "1.0" + max_version_exclusive: "2.0" +``` + +Unknown fields and incompatible ranges fail closed. + +## 4. Capabilities + +Extensions may declare: + +```yaml +capabilities: + provided: + - runtime.run.create + required: + - management.query +``` + +Capability declaration does not imply authorization. + +## 5. Permissions + +Permission families: + +```text +filesystem.read +filesystem.write +git.read +git.commit +git.push +process.spawn +network.connect +secrets.read +``` + +Scoped permissions use explicit lists, for example: + +```yaml +permissions: + filesystem: + read: ["src/**"] + write: ["tests/**"] + git: + read: true + commit: true + push: false + process: + spawn: true + network: + allow: ["127.0.0.1"] + secrets: + read: [] +``` + +## 6. Policy outcomes + +The policy evaluator returns: + +```text +allow +deny +approval_required +``` + +Default policy may be deny. + +Aggregate behavior: + +- any deny -> deny; +- otherwise any approval_required -> approval_required; +- otherwise allow. + +## 7. Enforcement boundary + +Permission evaluation is not a sandbox. + +The SDK explicitly reports: + +```text +enforcement.claimed = false +``` + +Actual enforcement must happen where Cortex controls the boundary, such as: + +- extension activation; +- RuntimePort invocation; +- governed tool invocation; +- privileged Management mutation; +- filesystem/process/network wrappers controlled by Cortex. + +If an external runtime can bypass Cortex, that limitation must remain visible. + +## 8. Entry points + +Supported manifest entry points: + +- server +- client +- cli +- worker + +Absolute paths and `..` traversal are rejected. + +## 9. Registry + +The in-memory reference registry is deterministic: + +- same id + same version -> idempotent; +- same id + different version -> explicit conflict. + +This is a contract/reference implementation, not a persistence owner. + +## 10. Existing systems remain authoritative + +M-040 does not replace: + +- Agent Adapter registry; +- catalog registry; +- Skill discovery; +- Host Adapter extension UI; +- project topology. + +Those systems may expose compatibility descriptors to the unified contract over time. + +## 11. Security invariants + +- no implicit execution from catalog content; +- no silent permission grants; +- unknown permission fields fail closed; +- capability != authorization; +- permission policy != sandbox; +- external enforcement gaps must be declared, not hidden. From 1e3b172370774c061c751fbf6e235b0c424da712 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:57:49 +0800 Subject: [PATCH 109/166] docs: index extension permission architecture --- docs/architecture/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 315fc63e..9288b2fa 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -33,6 +33,7 @@ - [Host / Runtime Topology and Capability Routing](./runtime-topology-routing.md) - [Control Service and Daemon Boundary](./control-service-boundary.md) - [CortexEvent v1 and Timeline Contract](./cortex-event-timeline.md) +- [Extension and Permission Architecture](./extension-permission-architecture.md) - [Branch Management Design](./branch-management-design.md) - [Catalog Bridge](./catalog-bridge.md) - [Context Optimization v2](./context-optimization-v2.md) From 8d84dc55e96303fa2c220b4ecdc5c00b83b5779a Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:22 +0800 Subject: [PATCH 110/166] feat(m040): add project-sdk package --- packages/project-sdk/package.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/project-sdk/package.json diff --git a/packages/project-sdk/package.json b/packages/project-sdk/package.json new file mode 100644 index 00000000..b6431f88 --- /dev/null +++ b/packages/project-sdk/package.json @@ -0,0 +1,17 @@ +{ + "name": "@cortex-agent/project-sdk", + "version": "0.0.0", + "private": true, + "description": "External project integration contracts for Cortex Agent", + "main": "src/index.js", + "exports": { + ".": "./src/index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "license": "MIT", + "dependencies": { + "@cortex-agent/protocol": "workspace:*" + } +} From 65466d7ff289f9d827288fcd307d674549684b79 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:25 +0800 Subject: [PATCH 111/166] feat(m040): define external Project Descriptor --- packages/project-sdk/src/descriptor.js | 234 +++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 packages/project-sdk/src/descriptor.js diff --git a/packages/project-sdk/src/descriptor.js b/packages/project-sdk/src/descriptor.js new file mode 100644 index 00000000..3cd7fd0e --- /dev/null +++ b/packages/project-sdk/src/descriptor.js @@ -0,0 +1,234 @@ +"use strict"; + +let protocol; +try { + protocol = require("@cortex-agent/protocol"); +} catch (_) { + protocol = require("../../protocol/src/index.js"); +} + +const PROJECT_DESCRIPTOR_SCHEMA_VERSION = "1"; +const PROJECT_INTEGRATION_MODES = Object.freeze([ + "embedded", + "connected", + "capability-bridge", +]); +const CORTEX_PROJECT_ROLES = Object.freeze([ + "governance-orchestration", + "governance-observer", +]); + +const TOP_KEYS = new Set([ + "schema_version", + "project_id", + "repository", + "integration_mode", + "capabilities", + "validation", + "artifacts", + "events", + "boundaries", +]); +const REPO_KEYS = new Set(["slug", "default_branch"]); +const CAP_KEYS = new Set(["provided", "required"]); +const VALIDATION_KEYS = new Set(["profiles"]); +const PROFILE_KEYS = new Set(["id", "command", "purpose", "blocking"]); +const ARTIFACT_KEYS = new Set(["id", "path", "kind"]); +const EVENTS_KEYS = new Set(["mode", "source"]); +const BOUNDARY_KEYS = new Set([ + "authoritative_domain", + "authoritative_runtime", + "cortex_role", + "protected_components", +]); + +class ProjectDescriptorError extends Error { + constructor(code, details = {}) { + super(`[project-descriptor:${code}] ${JSON.stringify(details)}`); + this.name = "ProjectDescriptorError"; + this.code = code; + this.details = details; + } +} + +function plain(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function rejectUnknown(value, known, where) { + for (const key of Object.keys(value || {})) { + if (!known.has(key)) { + throw new ProjectDescriptorError("ERR_PROJECT_FIELD_UNKNOWN", { where, key }); + } + } +} + +function string(value, where) { + if (typeof value !== "string" || !value.trim() || /[\r\n]/.test(value)) { + throw new ProjectDescriptorError("ERR_PROJECT_FIELD_INVALID", { where }); + } + return value.trim(); +} + +function id(value, where) { + const result = string(value, where); + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(result)) { + throw new ProjectDescriptorError("ERR_PROJECT_ID_INVALID", { where, value }); + } + return result; +} + +function relativePath(value, where) { + const result = string(value, where).replace(/\\/g, "/"); + if (result.startsWith("/") || /^[A-Za-z]:\//.test(result) || result.split("/").includes("..")) { + throw new ProjectDescriptorError("ERR_PROJECT_PATH_INVALID", { where, value }); + } + return result; +} + +function normalizeRepository(input) { + if (!plain(input)) throw new ProjectDescriptorError("ERR_PROJECT_REPOSITORY", {}); + rejectUnknown(input, REPO_KEYS, "repository"); + const slug = string(input.slug, "repository.slug"); + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(slug)) { + throw new ProjectDescriptorError("ERR_PROJECT_REPOSITORY_SLUG", { slug }); + } + return Object.freeze({ + slug, + default_branch: string(input.default_branch || "main", "repository.default_branch"), + }); +} + +function normalizeCapabilities(input) { + if (input == null) input = {}; + if (!plain(input)) throw new ProjectDescriptorError("ERR_PROJECT_CAPABILITIES", {}); + rejectUnknown(input, CAP_KEYS, "capabilities"); + const provided = protocol.validateCapabilityList(input.provided || []); + const required = protocol.validateCapabilityList(input.required || []); + for (const capability of [...provided, ...required]) { + const parsed = protocol.parseCapabilityId(capability); + if (!parsed || !["project", "management", "extension"].includes(parsed.namespace)) { + throw new ProjectDescriptorError("ERR_PROJECT_CAPABILITY_NAMESPACE", { capability }); + } + } + return Object.freeze({ provided, required }); +} + +function normalizeValidation(input) { + if (input == null) input = {}; + if (!plain(input)) throw new ProjectDescriptorError("ERR_PROJECT_VALIDATION", {}); + rejectUnknown(input, VALIDATION_KEYS, "validation"); + if (!Array.isArray(input.profiles || [])) { + throw new ProjectDescriptorError("ERR_PROJECT_VALIDATION_PROFILES", {}); + } + const seen = new Set(); + const profiles = (input.profiles || []).map((profile, index) => { + if (!plain(profile)) { + throw new ProjectDescriptorError("ERR_PROJECT_VALIDATION_PROFILE", { index }); + } + rejectUnknown(profile, PROFILE_KEYS, `validation.profiles[${index}]`); + const profileId = id(profile.id, `validation.profiles[${index}].id`); + if (seen.has(profileId)) { + throw new ProjectDescriptorError("ERR_PROJECT_VALIDATION_PROFILE_DUPLICATE", { id: profileId }); + } + seen.add(profileId); + return Object.freeze({ + id: profileId, + command: string(profile.command, `validation.profiles[${index}].command`), + purpose: string(profile.purpose, `validation.profiles[${index}].purpose`), + blocking: profile.blocking !== false, + }); + }); + return Object.freeze({ profiles: Object.freeze(profiles) }); +} + +function normalizeArtifacts(input) { + if (input == null) return Object.freeze([]); + if (!Array.isArray(input)) throw new ProjectDescriptorError("ERR_PROJECT_ARTIFACTS", {}); + const seen = new Set(); + return Object.freeze(input.map((artifact, index) => { + if (!plain(artifact)) throw new ProjectDescriptorError("ERR_PROJECT_ARTIFACT", { index }); + rejectUnknown(artifact, ARTIFACT_KEYS, `artifacts[${index}]`); + const artifactId = id(artifact.id, `artifacts[${index}].id`); + if (seen.has(artifactId)) { + throw new ProjectDescriptorError("ERR_PROJECT_ARTIFACT_DUPLICATE", { id: artifactId }); + } + seen.add(artifactId); + return Object.freeze({ + id: artifactId, + path: relativePath(artifact.path, `artifacts[${index}].path`), + kind: id(artifact.kind, `artifacts[${index}].kind`), + }); + })); +} + +function normalizeEvents(input) { + if (input == null) return Object.freeze({ mode: "none", source: null }); + if (!plain(input)) throw new ProjectDescriptorError("ERR_PROJECT_EVENTS", {}); + rejectUnknown(input, EVENTS_KEYS, "events"); + const mode = input.mode || "none"; + if (!["none", "observational"].includes(mode)) { + throw new ProjectDescriptorError("ERR_PROJECT_EVENT_MODE", { mode }); + } + return Object.freeze({ + mode, + source: input.source == null ? null : string(input.source, "events.source"), + }); +} + +function normalizeBoundaries(input) { + if (!plain(input)) throw new ProjectDescriptorError("ERR_PROJECT_BOUNDARIES", {}); + rejectUnknown(input, BOUNDARY_KEYS, "boundaries"); + const role = string(input.cortex_role, "boundaries.cortex_role"); + if (!CORTEX_PROJECT_ROLES.includes(role)) { + throw new ProjectDescriptorError("ERR_PROJECT_CORTEX_ROLE", { role }); + } + if (!Array.isArray(input.protected_components || [])) { + throw new ProjectDescriptorError("ERR_PROJECT_PROTECTED_COMPONENTS", {}); + } + return Object.freeze({ + authoritative_domain: string(input.authoritative_domain, "boundaries.authoritative_domain"), + authoritative_runtime: string(input.authoritative_runtime, "boundaries.authoritative_runtime"), + cortex_role: role, + protected_components: Object.freeze( + [...new Set((input.protected_components || []).map((value, index) => + id(value, `boundaries.protected_components[${index}]`)))], + ), + }); +} + +function normalizeProjectDescriptor(input) { + if (!plain(input)) throw new ProjectDescriptorError("ERR_PROJECT_DESCRIPTOR_INVALID", {}); + rejectUnknown(input, TOP_KEYS, "descriptor"); + const schemaVersion = input.schema_version == null + ? PROJECT_DESCRIPTOR_SCHEMA_VERSION + : String(input.schema_version); + if (schemaVersion !== PROJECT_DESCRIPTOR_SCHEMA_VERSION) { + throw new ProjectDescriptorError("ERR_PROJECT_SCHEMA_VERSION", { received: schemaVersion }); + } + const projectId = id(input.project_id, "project_id"); + const mode = string(input.integration_mode, "integration_mode"); + if (!PROJECT_INTEGRATION_MODES.includes(mode)) { + throw new ProjectDescriptorError("ERR_PROJECT_INTEGRATION_MODE", { mode }); + } + return Object.freeze({ + schema_version: PROJECT_DESCRIPTOR_SCHEMA_VERSION, + project_id: projectId, + project_ref: protocol.createRef("project", projectId), + repository: normalizeRepository(input.repository), + integration_mode: mode, + capabilities: normalizeCapabilities(input.capabilities), + validation: normalizeValidation(input.validation), + artifacts: normalizeArtifacts(input.artifacts), + events: normalizeEvents(input.events), + boundaries: normalizeBoundaries(input.boundaries), + }); +} + +module.exports = { + PROJECT_DESCRIPTOR_SCHEMA_VERSION, + PROJECT_INTEGRATION_MODES, + CORTEX_PROJECT_ROLES, + ProjectDescriptorError, + normalizeProjectDescriptor, +}; From 9ac4980192c10ad74456f4dc1f8c5d77dd69d68b Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:29 +0800 Subject: [PATCH 112/166] feat(m040): define Project Adapter contract --- packages/project-sdk/src/adapter.js | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/project-sdk/src/adapter.js diff --git a/packages/project-sdk/src/adapter.js b/packages/project-sdk/src/adapter.js new file mode 100644 index 00000000..a0f39e59 --- /dev/null +++ b/packages/project-sdk/src/adapter.js @@ -0,0 +1,67 @@ +"use strict"; + +const { normalizeProjectDescriptor, ProjectDescriptorError } = require("./descriptor"); + +const PROJECT_ADAPTER_CAPABILITIES = Object.freeze([ + "project.discover", + "project.validation.list", + "project.validation.run", + "project.artifact.list", + "project.event.read", +]); + +const METHOD_CAPABILITY = Object.freeze({ + discoverProject: "project.discover", + listValidationProfiles: "project.validation.list", + runValidation: "project.validation.run", + listArtifacts: "project.artifact.list", + readEvents: "project.event.read", +}); + +function createProjectAdapter(options = {}) { + const descriptor = normalizeProjectDescriptor(options.descriptor || {}); + const operations = options.operations || {}; + const declared = new Set(descriptor.capabilities.provided); + + const adapter = { descriptor }; + for (const [method, capability] of Object.entries(METHOD_CAPABILITY)) { + const implementation = operations[method]; + if (declared.has(capability) && typeof implementation !== "function") { + throw new ProjectDescriptorError("ERR_PROJECT_OPERATION_MISSING", { method, capability }); + } + adapter[method] = declared.has(capability) + ? implementation + : () => { + throw new ProjectDescriptorError("ERR_PROJECT_CAPABILITY_UNSUPPORTED", { + method, + capability, + }); + }; + } + return Object.freeze(adapter); +} + +function createDescriptorOnlyProjectAdapter(descriptorInput) { + const descriptor = normalizeProjectDescriptor(descriptorInput); + const capabilities = new Set(descriptor.capabilities.provided); + const operations = {}; + + if (capabilities.has("project.discover")) { + operations.discoverProject = () => descriptor; + } + if (capabilities.has("project.validation.list")) { + operations.listValidationProfiles = () => descriptor.validation.profiles; + } + if (capabilities.has("project.artifact.list")) { + operations.listArtifacts = () => descriptor.artifacts; + } + + return createProjectAdapter({ descriptor, operations }); +} + +module.exports = { + PROJECT_ADAPTER_CAPABILITIES, + METHOD_CAPABILITY, + createProjectAdapter, + createDescriptorOnlyProjectAdapter, +}; From af1f97f315afcf88aefff2de4c4f405136ae851e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:36 +0800 Subject: [PATCH 113/166] feat(m040): export Project SDK --- packages/project-sdk/src/index.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/project-sdk/src/index.js diff --git a/packages/project-sdk/src/index.js b/packages/project-sdk/src/index.js new file mode 100644 index 00000000..48b296fa --- /dev/null +++ b/packages/project-sdk/src/index.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + ...require("./descriptor"), + ...require("./adapter"), +}; From 4b1a1e42d2669e22fe4369fbcdc2a4c223548757 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:48 +0800 Subject: [PATCH 114/166] test(m040): cover Project Integration contract --- tests/project-sdk/project-sdk.test.js | 117 ++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/project-sdk/project-sdk.test.js diff --git a/tests/project-sdk/project-sdk.test.js b/tests/project-sdk/project-sdk.test.js new file mode 100644 index 00000000..b7e1c0a1 --- /dev/null +++ b/tests/project-sdk/project-sdk.test.js @@ -0,0 +1,117 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const project = require(path.join(ROOT, "packages", "project-sdk", "src")); + +function descriptor(overrides = {}) { + return { + project_id: "axrail", + repository: { + slug: "Kucell/axrail", + default_branch: "main", + }, + integration_mode: "connected", + capabilities: { + provided: [ + "project.discover", + "project.validation.list", + "project.validation.run", + "project.artifact.list", + "project.event.read", + ], + required: [], + }, + validation: { + profiles: [ + { + id: "release-dry-run", + command: "pnpm release:dry-run", + purpose: "Axrail release gate", + blocking: true, + }, + ], + }, + artifacts: [ + { id: "architecture", path: "docs/architecture/README.md", kind: "architecture" }, + ], + events: { + mode: "observational", + source: "@axrail/events", + }, + boundaries: { + authoritative_domain: "axrail", + authoritative_runtime: "axrail", + cortex_role: "governance-orchestration", + protected_components: [ + "HarnessRuntime", + "ToolRuntime", + "Policy", + "Validation", + "Approval", + "TransactionRuntime", + "EventStore", + ], + }, + ...overrides, + }; +} + +test("connected project descriptor preserves external project authority", () => { + const value = project.normalizeProjectDescriptor(descriptor()); + assert.equal(value.project_ref, "project:axrail"); + assert.equal(value.integration_mode, "connected"); + assert.equal(value.boundaries.authoritative_domain, "axrail"); + assert.equal(value.boundaries.authoritative_runtime, "axrail"); + assert.ok(value.boundaries.protected_components.includes("TransactionRuntime")); +}); + +test("project validation profiles are declarative and do not execute by themselves", () => { + const adapter = project.createDescriptorOnlyProjectAdapter(descriptor({ + capabilities: { + provided: [ + "project.discover", + "project.validation.list", + "project.artifact.list", + ], + required: [], + }, + })); + assert.equal(adapter.listValidationProfiles()[0].command, "pnpm release:dry-run"); + assert.throws( + () => adapter.runValidation("release-dry-run"), + (error) => error.code === "ERR_PROJECT_CAPABILITY_UNSUPPORTED", + ); +}); + +test("declared executable project capabilities require an explicit operation owner", () => { + assert.throws( + () => project.createProjectAdapter({ descriptor: descriptor(), operations: {} }), + (error) => error.code === "ERR_PROJECT_OPERATION_MISSING" + && error.details.capability === "project.discover", + ); +}); + +test("project artifact paths reject traversal", () => { + assert.throws( + () => project.normalizeProjectDescriptor(descriptor({ + artifacts: [{ id: "escape", path: "../secret", kind: "evidence" }], + })), + (error) => error.code === "ERR_PROJECT_PATH_INVALID", + ); +}); + +test("unknown or runtime-specific capabilities cannot leak into Project Integration", () => { + assert.throws( + () => project.normalizeProjectDescriptor(descriptor({ + capabilities: { + provided: ["runtime.run.create"], + required: [], + }, + })), + (error) => error.code === "ERR_PROJECT_CAPABILITY_NAMESPACE", + ); +}); From 337c6629af406def2cdc214516d3d0f3fd5ab42e Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:55 +0800 Subject: [PATCH 115/166] chore(m040): register project-sdk workspace --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3131b429..c75b9d1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,3 +27,9 @@ importers: '@cortex-agent/protocol': specifier: workspace:* version: link:../protocol + + packages/project-sdk: + dependencies: + '@cortex-agent/protocol': + specifier: workspace:* + version: link:../protocol From 67d639ea38a61b9adb0870390b88b90950a15abd Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:58 +0800 Subject: [PATCH 116/166] test(m040): include Project SDK suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4bc316e8..a7d42af5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js tests/extensions/extension-sdk.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js tests/extensions/extension-sdk.test.js tests/project-sdk/project-sdk.test.js" }, "keywords": [ "ai", From cfbd445bd07a28b24c7f203c5efe422a24f04cf8 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:02:01 +0800 Subject: [PATCH 117/166] ci(m040): validate Project Integration changes --- .github/workflows/m040-validation.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/m040-validation.yml b/.github/workflows/m040-validation.yml index 5d3eb5d5..dc294f9c 100644 --- a/.github/workflows/m040-validation.yml +++ b/.github/workflows/m040-validation.yml @@ -21,6 +21,7 @@ on: - "tests/control-service/**" - "tests/event-bridge/**" - "tests/extensions/**" + - "tests/project-sdk/**" - "tests/management/management-query-cli.test.js" - ".github/workflows/m040-validation.yml" pull_request: @@ -30,10 +31,18 @@ on: - "pnpm-workspace.yaml" - "packages/**" - "lib/sdk/**" + - "lib/event-bridge/**" + - "lib/control-service/**" + - "lib/runtime-port/**" - "lib/commands/management/query.js" - "tests/architecture/**" - "tests/protocol/**" - "tests/sdk/**" + - "tests/project-sdk/**" + - "tests/extensions/**" + - "tests/event-bridge/**" + - "tests/control-service/**" + - "tests/runtime-port/**" - "tests/management/management-query-cli.test.js" - ".github/workflows/m040-validation.yml" From 96cab1f71b23237f4147de2138a140ecbcb7fec6 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:04:08 +0800 Subject: [PATCH 118/166] feat(m040): validate connected project descriptors --- scripts/m040/validate-connected-project.js | 124 +++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 scripts/m040/validate-connected-project.js diff --git a/scripts/m040/validate-connected-project.js b/scripts/m040/validate-connected-project.js new file mode 100644 index 00000000..e9d11281 --- /dev/null +++ b/scripts/m040/validate-connected-project.js @@ -0,0 +1,124 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { + normalizeProjectDescriptor, +} = require("../../packages/project-sdk/src/index.js"); + +function fail(message, details = {}) { + const error = new Error(message); + error.code = "ERR_CONNECTED_PROJECT_VALIDATION"; + error.details = details; + throw error; +} + +function parsePnpmScript(command) { + const match = /^pnpm\s+([A-Za-z0-9:_-]+)$/.exec(command.trim()); + return match ? match[1] : null; +} + +function validateConnectedProject(projectRoot, options = {}) { + const descriptorPath = path.join(projectRoot, "cortex.project.json"); + const packagePath = path.join(projectRoot, "package.json"); + + if (!fs.existsSync(descriptorPath)) fail("cortex.project.json missing", { descriptorPath }); + const rawDescriptor = JSON.parse(fs.readFileSync(descriptorPath, "utf8")); + const descriptor = normalizeProjectDescriptor(rawDescriptor); + + if (descriptor.integration_mode !== "connected") { + fail("pilot project must use connected integration mode", { + integration_mode: descriptor.integration_mode, + }); + } + if (options.expectProject && descriptor.project_id !== options.expectProject) { + fail("unexpected project id", { + expected: options.expectProject, + actual: descriptor.project_id, + }); + } + if (options.expectRepository && descriptor.repository.slug !== options.expectRepository) { + fail("unexpected repository slug", { + expected: options.expectRepository, + actual: descriptor.repository.slug, + }); + } + + const packageJson = fs.existsSync(packagePath) + ? JSON.parse(fs.readFileSync(packagePath, "utf8")) + : null; + const scripts = packageJson && packageJson.scripts ? packageJson.scripts : {}; + + const validationProfiles = descriptor.validation.profiles.map((profile) => { + const script = parsePnpmScript(profile.command); + if (!script) { + fail("pilot only accepts single pnpm script validation commands", { + profile: profile.id, + command: profile.command, + }); + } + if (!Object.prototype.hasOwnProperty.call(scripts, script)) { + fail("declared pnpm validation script does not exist", { + profile: profile.id, + script, + }); + } + return { + id: profile.id, + script, + blocking: profile.blocking, + command: profile.command, + }; + }); + + const artifacts = descriptor.artifacts.map((artifact) => { + const absolute = path.resolve(projectRoot, artifact.path); + const relative = path.relative(projectRoot, absolute); + if (relative.startsWith("..") || path.isAbsolute(relative)) { + fail("artifact escaped project root", { artifact: artifact.id }); + } + if (!fs.existsSync(absolute)) { + fail("declared artifact does not exist", { + artifact: artifact.id, + path: artifact.path, + }); + } + return { + id: artifact.id, + path: artifact.path, + kind: artifact.kind, + }; + }); + + return { + ok: true, + project_id: descriptor.project_id, + project_ref: descriptor.project_ref, + repository: descriptor.repository.slug, + integration_mode: descriptor.integration_mode, + cortex_role: descriptor.boundaries.cortex_role, + authoritative_domain: descriptor.boundaries.authoritative_domain, + authoritative_runtime: descriptor.boundaries.authoritative_runtime, + protected_components: descriptor.boundaries.protected_components, + validation_profiles: validationProfiles, + artifacts, + events: descriptor.events, + }; +} + +if (require.main === module) { + const args = process.argv.slice(2); + const root = args[0] ? path.resolve(args[0]) : process.cwd(); + const projectAt = args.indexOf("--expect-project"); + const repoAt = args.indexOf("--expect-repository"); + const result = validateConnectedProject(root, { + expectProject: projectAt >= 0 ? args[projectAt + 1] : null, + expectRepository: repoAt >= 0 ? args[repoAt + 1] : null, + }); + process.stdout.write(JSON.stringify(result, null, 2) + "\n"); +} + +module.exports = { + parsePnpmScript, + validateConnectedProject, +}; From f43a75a132dea6d10cf96ae006d6fb93e21d1674 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:04:10 +0800 Subject: [PATCH 119/166] test(m040): cover connected project validator --- .../connected-project-validator.test.js | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/project-integration/connected-project-validator.test.js diff --git a/tests/project-integration/connected-project-validator.test.js b/tests/project-integration/connected-project-validator.test.js new file mode 100644 index 00000000..34aa1a02 --- /dev/null +++ b/tests/project-integration/connected-project-validator.test.js @@ -0,0 +1,89 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const { + parsePnpmScript, + validateConnectedProject, +} = require(path.join(ROOT, "scripts", "m040", "validate-connected-project.js")); + +function fixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "m040-project-")); + fs.mkdirSync(path.join(root, "docs"), { recursive: true }); + fs.writeFileSync(path.join(root, "docs", "architecture.md"), "# architecture\n"); + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ + scripts: { check: "echo ok" }, + })); + fs.writeFileSync(path.join(root, "cortex.project.json"), JSON.stringify({ + project_id: "demo", + repository: { slug: "Kucell/demo", default_branch: "main" }, + integration_mode: "connected", + capabilities: { + provided: ["project.discover", "project.validation.list", "project.artifact.list"], + required: [], + }, + validation: { + profiles: [{ + id: "check", + command: "pnpm check", + purpose: "check", + blocking: true, + }], + }, + artifacts: [{ + id: "architecture", + path: "docs/architecture.md", + kind: "architecture", + }], + events: { mode: "none" }, + boundaries: { + authoritative_domain: "demo", + authoritative_runtime: "demo", + cortex_role: "governance-orchestration", + protected_components: ["Runtime"], + }, + })); + return root; +} + +test("connected project validator checks declared pnpm scripts and artifacts without executing them", () => { + const root = fixture(); + try { + const result = validateConnectedProject(root, { + expectProject: "demo", + expectRepository: "Kucell/demo", + }); + assert.equal(result.ok, true); + assert.equal(result.validation_profiles[0].script, "check"); + assert.equal(result.artifacts[0].path, "docs/architecture.md"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("connected project validator rejects missing declared scripts", () => { + const root = fixture(); + try { + const descriptorPath = path.join(root, "cortex.project.json"); + const descriptor = JSON.parse(fs.readFileSync(descriptorPath, "utf8")); + descriptor.validation.profiles[0].command = "pnpm missing"; + fs.writeFileSync(descriptorPath, JSON.stringify(descriptor)); + assert.throws( + () => validateConnectedProject(root), + (error) => error.code === "ERR_CONNECTED_PROJECT_VALIDATION", + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("validator accepts only simple pnpm script declarations", () => { + assert.equal(parsePnpmScript("pnpm check"), "check"); + assert.equal(parsePnpmScript("pnpm release:dry-run"), "release:dry-run"); + assert.equal(parsePnpmScript("pnpm check && rm -rf /"), null); +}); From 3e211b303e1f0062b2cf3dbabf10f423c2333d53 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:04:13 +0800 Subject: [PATCH 120/166] test(m040): cover explicit Project Adapter execution owner --- tests/project-sdk/project-sdk.test.js | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/project-sdk/project-sdk.test.js b/tests/project-sdk/project-sdk.test.js index b7e1c0a1..e9235594 100644 --- a/tests/project-sdk/project-sdk.test.js +++ b/tests/project-sdk/project-sdk.test.js @@ -115,3 +115,37 @@ test("unknown or runtime-specific capabilities cannot leak into Project Integrat (error) => error.code === "ERR_PROJECT_CAPABILITY_NAMESPACE", ); }); + +test("Project Adapter execution remains behind an explicit operation owner", async () => { + const calls = []; + const value = descriptor(); + const adapter = project.createProjectAdapter({ + descriptor: value, + operations: { + discoverProject() { return project.normalizeProjectDescriptor(value); }, + listValidationProfiles() { + return project.normalizeProjectDescriptor(value).validation.profiles; + }, + async runValidation(profileId, context) { + calls.push({ profileId, context }); + return { status: "passed", evidence_ref: "ci:axrail:123" }; + }, + listArtifacts() { + return project.normalizeProjectDescriptor(value).artifacts; + }, + readEvents() { + return []; + }, + }, + }); + + const result = await adapter.runValidation("release-dry-run", { + authorization_ref: "decision:D-AXRAIL", + }); + assert.equal(result.status, "passed"); + assert.equal(result.evidence_ref, "ci:axrail:123"); + assert.deepEqual(calls, [{ + profileId: "release-dry-run", + context: { authorization_ref: "decision:D-AXRAIL" }, + }]); +}); From 19cc12a4623dd0ca0e28643d4776b6c9cb256021 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:04:16 +0800 Subject: [PATCH 121/166] test(m040): include connected project validation suite --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7d42af5..afa6ad4c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "release:major": "npm version major && npm publish --registry https://registry.npmjs.org/", "pub": "npm publish --registry https://registry.npmjs.org/", "heartbeat": "node .agent/scripts/heartbeat.js", - "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js tests/extensions/extension-sdk.test.js tests/project-sdk/project-sdk.test.js" + "test:m040": "node --test tests/architecture/m040-monorepo-boundary.test.js tests/protocol/capabilities.test.js tests/protocol/negotiation.test.js tests/sdk/cortex-client.test.js tests/sdk/local-transport.test.js tests/runtime-port/runtime-port.test.js tests/runtime-port/topology.test.js tests/runtime-port/native-endpoint.test.js tests/runtime-port/capability-router.test.js tests/runtime-port/legacy-adapter-bridge.test.js tests/management/management-query-cli.test.js tests/control-service/control-service.test.js tests/control-service/control-boundary.test.js tests/control-service/local-control-service.test.js tests/protocol/events.test.js tests/event-bridge/event-bridge.test.js tests/extensions/extension-sdk.test.js tests/project-sdk/project-sdk.test.js tests/project-integration/connected-project-validator.test.js" }, "keywords": [ "ai", From 76d2d40392238cb4920bd07ef9733ddc22311ced Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:05:02 +0800 Subject: [PATCH 122/166] fix(m040): avoid revalidating computed ProjectRef --- packages/project-sdk/src/adapter.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/project-sdk/src/adapter.js b/packages/project-sdk/src/adapter.js index a0f39e59..c6ee59be 100644 --- a/packages/project-sdk/src/adapter.js +++ b/packages/project-sdk/src/adapter.js @@ -56,7 +56,10 @@ function createDescriptorOnlyProjectAdapter(descriptorInput) { operations.listArtifacts = () => descriptor.artifacts; } - return createProjectAdapter({ descriptor, operations }); + // Pass the original closed-schema input into createProjectAdapter(). + // The normalized descriptor contains the computed project_ref field, which + // intentionally is not accepted as external manifest input. + return createProjectAdapter({ descriptor: descriptorInput, operations }); } module.exports = { From bd71a18219b4e461f0782d1627f3d8bbd21ed170 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:06:14 +0800 Subject: [PATCH 123/166] ci(m040): validate Axrail connected-project pilot --- .github/workflows/m040-axrail-pilot.yml | 107 ++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/m040-axrail-pilot.yml diff --git a/.github/workflows/m040-axrail-pilot.yml b/.github/workflows/m040-axrail-pilot.yml new file mode 100644 index 00000000..e6c0fea0 --- /dev/null +++ b/.github/workflows/m040-axrail-pilot.yml @@ -0,0 +1,107 @@ +name: M-040 Axrail Connected Project Pilot + +on: + push: + branches: + - "feat/m040-*" + paths: + - "packages/project-sdk/**" + - "scripts/m040/**" + - "tests/project-sdk/**" + - "tests/project-integration/**" + - ".github/workflows/m040-axrail-pilot.yml" + pull_request: + paths: + - "packages/project-sdk/**" + - "scripts/m040/**" + - "tests/project-sdk/**" + - "tests/project-integration/**" + - ".github/workflows/m040-axrail-pilot.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + axrail-connected-project: + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Checkout Cortex + uses: actions/checkout@v4 + with: + path: cortex + + - name: Checkout Axrail pilot + uses: actions/checkout@v4 + with: + repository: Kucell/axrail + ref: feat/cortex-project-integration-pilot + path: axrail + + - name: Setup Node for descriptor validation + uses: actions/setup-node@v4 + with: + node-version: "24.19.0" + + - name: Validate Axrail connected-project descriptor + working-directory: cortex + run: | + node scripts/m040/validate-connected-project.js ../axrail --expect-project axrail --expect-repository Kucell/axrail | tee ../axrail-cortex-project-validation.json + + - name: Verify Axrail protected authority boundary + working-directory: cortex + run: | + node - <<'NODE' + const fs = require("node:fs"); + const result = JSON.parse(fs.readFileSync("../axrail-cortex-project-validation.json", "utf8")); + const required = [ + "HarnessRuntime", + "ToolRuntime", + "Policy", + "Validation", + "Approval", + "TransactionRuntime", + "EventStore", + "EngineeringAdapters", + ]; + for (const name of required) { + if (!result.protected_components.includes(name)) { + throw new Error("missing protected Axrail authority: " + name); + } + } + if (result.authoritative_domain !== "axrail" || result.authoritative_runtime !== "axrail") { + throw new Error("Axrail authority boundary drift"); + } + if (result.cortex_role !== "governance-orchestration") { + throw new Error("unexpected Cortex role: " + result.cortex_role); + } + NODE + + - name: Enable Axrail pnpm + working-directory: axrail + run: | + corepack enable + corepack prepare pnpm@10.6.5 --activate + pnpm --version + + - name: Install Axrail workspace + working-directory: axrail + run: pnpm install --frozen-lockfile + + - name: Execute declared Axrail release validation profile + working-directory: axrail + run: | + pnpm release:dry-run 2>&1 | tee ../axrail-release-dry-run.log + + - name: Upload pilot evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: m040-axrail-connected-project-evidence + path: | + axrail-cortex-project-validation.json + axrail-release-dry-run.log + if-no-files-found: error + retention-days: 14 From 2a9a147fa58a9639d66ef5398c7256527e150371 Mon Sep 17 00:00:00 2001 From: Kucell <50976390+Kucell@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:07:23 +0800 Subject: [PATCH 124/166] docs: define M-040 external project integration --- docs/architecture/project-integration.md | 182 +++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/architecture/project-integration.md diff --git a/docs/architecture/project-integration.md b/docs/architecture/project-integration.md new file mode 100644 index 00000000..fe8d2d9e --- /dev/null +++ b/docs/architecture/project-integration.md @@ -0,0 +1,182 @@ +# External Project Integration Contract + +> **Status**: M-040 MS-009 baseline +> **Reference pilot**: Kucell/axrail + +## 1. Purpose + +Cortex can govern software projects other than `cortex-agent` without requiring those projects to adopt Cortex's internal runtime/domain architecture. + +Project Integration is separate from Runtime Integration. + +```text +Cortex Governance + ├─ Project Integration -> Axrail / HMI / Industra / autopeer / ... + └─ Runtime Integration -> Native adapters / Paseo / future runtimes +``` + +Axrail is a project integration reference, not a RuntimePort backend. + +## 2. Integration modes + +### Embedded + +The project installs/owns Cortex `.agent` governance directly. + +### Connected + +The project remains autonomous and exposes a stable `cortex.project.json` descriptor. + +This is the Axrail pilot mode. + +### Capability Bridge + +The project provides a Project Adapter/Provider that implements selected capabilities behind explicit operations. + +## 3. Project Descriptor + +The portable `@cortex-agent/project-sdk` contract describes: + +- project identity; +- repository identity; +- integration mode; +- provided/required capabilities; +- validation profiles; +- evidence/artifact paths; +- observational event source; +- authority boundaries. + +It computes a canonical `ProjectRef`. + +## 4. Project capabilities + +Initial capability family: + +```text +project.discover +project.validation.list +project.validation.run +project.artifact.list +project.event.read +``` + +A descriptor may declare a capability, but executable capabilities require a Project Adapter operation implementation. + +## 5. Validation profiles + +A connected project may declare validation profiles: + +```json +{ + "id": "release-dry-run", + "command": "pnpm release:dry-run", + "purpose": "release gate", + "blocking": true +} +``` + +The descriptor is data. + +Reading it never executes the command. + +Execution requires a controlled Project Adapter/CI boundary and remains subject to Cortex permission/approval policy. + +The M-040 pilot validator deliberately accepts only simple `pnpm