@cleocode/contracts 2026.5.129 → 2026.5.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/atomic-tools-contract.test.d.ts +14 -0
- package/dist/__tests__/atomic-tools-contract.test.d.ts.map +1 -0
- package/dist/__tests__/atomic-tools-contract.test.js +55 -0
- package/dist/__tests__/atomic-tools-contract.test.js.map +1 -0
- package/dist/__tests__/gateway-contract.test.d.ts +18 -0
- package/dist/__tests__/gateway-contract.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-contract.test.js +187 -0
- package/dist/__tests__/gateway-contract.test.js.map +1 -0
- package/dist/__tests__/mvi.test.d.ts +17 -0
- package/dist/__tests__/mvi.test.d.ts.map +1 -0
- package/dist/__tests__/mvi.test.js +147 -0
- package/dist/__tests__/mvi.test.js.map +1 -0
- package/dist/__tests__/operation-def.test.js +1 -1
- package/dist/__tests__/operation-def.test.js.map +1 -1
- package/dist/boundary.d.ts +26 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +42 -4
- package/dist/boundary.js.map +1 -1
- package/dist/branch-lock.d.ts +35 -1
- package/dist/branch-lock.d.ts.map +1 -1
- package/dist/branch-lock.js +23 -2
- package/dist/branch-lock.js.map +1 -1
- package/dist/daemon/__tests__/health-roundtrip.test.d.ts +13 -0
- package/dist/daemon/__tests__/health-roundtrip.test.d.ts.map +1 -0
- package/dist/daemon/__tests__/health-roundtrip.test.js +69 -0
- package/dist/daemon/__tests__/health-roundtrip.test.js.map +1 -0
- package/dist/daemon/health.d.ts +108 -0
- package/dist/daemon/health.d.ts.map +1 -0
- package/dist/daemon/health.js +95 -0
- package/dist/daemon/health.js.map +1 -0
- package/dist/daemon/index.d.ts +24 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +22 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/subsystem.d.ts +121 -0
- package/dist/daemon/subsystem.d.ts.map +1 -0
- package/dist/daemon/subsystem.js +28 -0
- package/dist/daemon/subsystem.js.map +1 -0
- package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.d.ts +15 -0
- package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.d.ts.map +1 -0
- package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.js +40 -0
- package/dist/daemon-ipc/__tests__/daemon-ipc-freeze.test.js.map +1 -0
- package/dist/daemon-ipc/index.d.ts +44 -0
- package/dist/daemon-ipc/index.d.ts.map +1 -0
- package/dist/daemon-ipc/index.js +43 -0
- package/dist/daemon-ipc/index.js.map +1 -0
- package/dist/db-inventory.json +8 -8
- package/dist/dispatch/identity.d.ts +1 -1
- package/dist/dispatch/identity.d.ts.map +1 -1
- package/dist/dispatch/identity.js +3 -0
- package/dist/dispatch/identity.js.map +1 -1
- package/dist/dispatch/operation-def.d.ts +16 -0
- package/dist/dispatch/operation-def.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.js +133 -0
- package/dist/dispatch/operations-registry.js.map +1 -1
- package/dist/exit-codes.d.ts +1 -1
- package/dist/exit-codes.js +1 -1
- package/dist/gateway/rpc/__tests__/freeze.test.d.ts +20 -0
- package/dist/gateway/rpc/__tests__/freeze.test.d.ts.map +1 -0
- package/dist/gateway/rpc/__tests__/freeze.test.js +122 -0
- package/dist/gateway/rpc/__tests__/freeze.test.js.map +1 -0
- package/dist/gateway/rpc/index.d.ts +23 -0
- package/dist/gateway/rpc/index.d.ts.map +1 -0
- package/dist/gateway/rpc/index.js +22 -0
- package/dist/gateway/rpc/index.js.map +1 -0
- package/dist/gateway/rpc/messages.d.ts +263 -0
- package/dist/gateway/rpc/messages.d.ts.map +1 -0
- package/dist/gateway/rpc/messages.js +152 -0
- package/dist/gateway/rpc/messages.js.map +1 -0
- package/dist/gateway/rpc/version.d.ts +39 -0
- package/dist/gateway/rpc/version.d.ts.map +1 -0
- package/dist/gateway/rpc/version.js +39 -0
- package/dist/gateway/rpc/version.js.map +1 -0
- package/dist/gateway.d.ts +239 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +171 -0
- package/dist/gateway.js.map +1 -0
- package/dist/goal.d.ts +314 -0
- package/dist/goal.d.ts.map +1 -0
- package/dist/goal.js +118 -0
- package/dist/goal.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/mvi.d.ts +209 -0
- package/dist/mvi.d.ts.map +1 -0
- package/dist/mvi.js +99 -0
- package/dist/mvi.js.map +1 -0
- package/dist/operations/attention.d.ts +114 -0
- package/dist/operations/attention.d.ts.map +1 -0
- package/dist/operations/attention.js +22 -0
- package/dist/operations/attention.js.map +1 -0
- package/dist/operations/docs.d.ts +7 -1
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/docs.js.map +1 -1
- package/dist/operations/focus.d.ts +25 -0
- package/dist/operations/focus.d.ts.map +1 -1
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.d.ts.map +1 -1
- package/dist/operations/index.js +1 -0
- package/dist/operations/index.js.map +1 -1
- package/dist/operations/session.d.ts +11 -0
- package/dist/operations/session.d.ts.map +1 -1
- package/dist/sdk-tool.d.ts +1 -1
- package/dist/sdk-tool.js +1 -1
- package/dist/supervisor-ipc/__tests__/freeze.test.d.ts +15 -0
- package/dist/supervisor-ipc/__tests__/freeze.test.d.ts.map +1 -0
- package/dist/supervisor-ipc/__tests__/freeze.test.js +159 -0
- package/dist/supervisor-ipc/__tests__/freeze.test.js.map +1 -0
- package/dist/supervisor-ipc/index.d.ts +15 -0
- package/dist/supervisor-ipc/index.d.ts.map +1 -0
- package/dist/supervisor-ipc/index.js +14 -0
- package/dist/supervisor-ipc/index.js.map +1 -0
- package/dist/supervisor-ipc/messages.d.ts +457 -0
- package/dist/supervisor-ipc/messages.d.ts.map +1 -0
- package/dist/supervisor-ipc/messages.js +312 -0
- package/dist/supervisor-ipc/messages.js.map +1 -0
- package/dist/supervisor-ipc/version.d.ts +29 -0
- package/dist/supervisor-ipc/version.d.ts.map +1 -0
- package/dist/supervisor-ipc/version.js +29 -0
- package/dist/supervisor-ipc/version.js.map +1 -0
- package/dist/tools/__tests__/composite.test.d.ts +14 -0
- package/dist/tools/__tests__/composite.test.d.ts.map +1 -0
- package/dist/tools/__tests__/composite.test.js +75 -0
- package/dist/tools/__tests__/composite.test.js.map +1 -0
- package/dist/tools/atomic.d.ts +195 -0
- package/dist/tools/atomic.d.ts.map +1 -0
- package/dist/tools/atomic.js +82 -0
- package/dist/tools/atomic.js.map +1 -0
- package/dist/tools/composite.d.ts +128 -0
- package/dist/tools/composite.d.ts.map +1 -0
- package/dist/tools/composite.js +88 -0
- package/dist/tools/composite.js.map +1 -0
- package/package.json +35 -3
- package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
- package/src/__tests__/atomic-tools-contract.test.ts +67 -0
- package/src/__tests__/gateway-contract.test.ts +219 -0
- package/src/__tests__/mvi.test.ts +175 -0
- package/src/__tests__/operation-def.test.ts +2 -1
- package/src/boundary.ts +74 -4
- package/src/branch-lock.ts +49 -2
- package/src/daemon/__tests__/health-roundtrip.test.ts +84 -0
- package/src/daemon/health.ts +121 -0
- package/src/daemon/index.ts +35 -0
- package/src/daemon/subsystem.ts +126 -0
- package/src/daemon-ipc/__tests__/daemon-ipc-freeze.test.ts +53 -0
- package/src/daemon-ipc/index.ts +90 -0
- package/src/db-inventory.json +8 -8
- package/src/dispatch/identity.ts +3 -0
- package/src/dispatch/operation-def.ts +16 -0
- package/src/dispatch/operations-registry.ts +140 -0
- package/src/exit-codes.ts +1 -1
- package/src/gateway/rpc/__tests__/freeze.test.ts +139 -0
- package/src/gateway/rpc/index.ts +43 -0
- package/src/gateway/rpc/messages.ts +187 -0
- package/src/gateway/rpc/version.ts +40 -0
- package/src/gateway.ts +252 -0
- package/src/goal.ts +348 -0
- package/src/index.ts +109 -0
- package/src/mvi.ts +236 -0
- package/src/operations/attention.ts +117 -0
- package/src/operations/docs.ts +7 -1
- package/src/operations/focus.ts +26 -0
- package/src/operations/index.ts +1 -0
- package/src/operations/session.ts +11 -0
- package/src/sdk-tool.ts +1 -1
- package/src/supervisor-ipc/__tests__/freeze.test.ts +180 -0
- package/src/supervisor-ipc/index.ts +62 -0
- package/src/supervisor-ipc/messages.ts +396 -0
- package/src/supervisor-ipc/version.ts +30 -0
- package/src/tools/__tests__/composite.test.ts +88 -0
- package/src/tools/atomic.ts +274 -0
- package/src/tools/composite.ts +173 -0
package/src/dispatch/identity.ts
CHANGED
|
@@ -96,6 +96,9 @@ export const CANONICAL_DOMAINS = [
|
|
|
96
96
|
'worktree',
|
|
97
97
|
// T9973: 'cleo focus <id>' — single-envelope task orientation (8 calls → 1).
|
|
98
98
|
'focus',
|
|
99
|
+
// T11373: 'cleo attention add|show|list' (alias 'jot') — Tier-2 scope-keyed
|
|
100
|
+
// working-memory buffer (Epic T11288 · Saga T11283).
|
|
101
|
+
'attention',
|
|
99
102
|
] as const;
|
|
100
103
|
|
|
101
104
|
/**
|
|
@@ -76,6 +76,22 @@ export interface OperationDef {
|
|
|
76
76
|
* @see T4897 for progressive migration
|
|
77
77
|
*/
|
|
78
78
|
params?: ParamDef[];
|
|
79
|
+
/**
|
|
80
|
+
* Whether this operation is surfaced as an MCP tool by the
|
|
81
|
+
* `@cleocode/runtime/gateway/mcp` transport adapter.
|
|
82
|
+
*
|
|
83
|
+
* **Default-deny**: absent / `false` means the operation is NOT exposed over
|
|
84
|
+
* MCP. The MCP adapter generates its `tools/list` from the subset of
|
|
85
|
+
* {@link OperationDef} entries that explicitly set `mcpExposed: true`, so the
|
|
86
|
+
* external tool surface is opt-in rather than the full CQRS registry. This
|
|
87
|
+
* preserves the historically-curated MCP surface (the standalone
|
|
88
|
+
* `@cleocode/mcp-adapter` exposed exactly 3 sentient tools) while letting new
|
|
89
|
+
* tools be promoted one registry edit at a time.
|
|
90
|
+
*
|
|
91
|
+
* @see {@link https://modelcontextprotocol.io | Model Context Protocol}
|
|
92
|
+
* @task T11448
|
|
93
|
+
*/
|
|
94
|
+
mcpExposed?: boolean;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
// ── Resolution ───────────────────────────────────────────────────────
|
|
@@ -7653,6 +7653,22 @@ export const OPERATIONS: OperationDef[] = [
|
|
|
7653
7653
|
// to public SDK consumers using @cleocode/core. Adding all 10 ops here.
|
|
7654
7654
|
// ---------------------------------------------------------------------------
|
|
7655
7655
|
|
|
7656
|
+
// sentient query: status (T11448 — exposed over MCP; mirrors the standalone
|
|
7657
|
+
// mcp-adapter `cleo_sentient_status` tool + the `cleo sentient status` CLI op).
|
|
7658
|
+
{
|
|
7659
|
+
gateway: 'query',
|
|
7660
|
+
domain: 'sentient',
|
|
7661
|
+
operation: 'status',
|
|
7662
|
+
description:
|
|
7663
|
+
'sentient.status (query) — daemon state, kill-switch status, Tier-2 enabled flag, and proposal statistics',
|
|
7664
|
+
tier: 2,
|
|
7665
|
+
idempotent: true,
|
|
7666
|
+
sessionRequired: false,
|
|
7667
|
+
requiredParams: [],
|
|
7668
|
+
params: [] satisfies ParamDef[],
|
|
7669
|
+
mcpExposed: true,
|
|
7670
|
+
},
|
|
7671
|
+
|
|
7656
7672
|
// sentient query: propose.list
|
|
7657
7673
|
{
|
|
7658
7674
|
gateway: 'query',
|
|
@@ -7672,6 +7688,7 @@ export const OPERATIONS: OperationDef[] = [
|
|
|
7672
7688
|
description: 'Maximum number of proposals to return (default: 50)',
|
|
7673
7689
|
},
|
|
7674
7690
|
] satisfies ParamDef[],
|
|
7691
|
+
mcpExposed: true,
|
|
7675
7692
|
},
|
|
7676
7693
|
|
|
7677
7694
|
// sentient query: propose.diff
|
|
@@ -7784,6 +7801,7 @@ export const OPERATIONS: OperationDef[] = [
|
|
|
7784
7801
|
sessionRequired: false,
|
|
7785
7802
|
requiredParams: [],
|
|
7786
7803
|
params: [] satisfies ParamDef[],
|
|
7804
|
+
mcpExposed: true,
|
|
7787
7805
|
},
|
|
7788
7806
|
|
|
7789
7807
|
// sentient mutate: propose.disable
|
|
@@ -8690,6 +8708,128 @@ export const OPERATIONS: OperationDef[] = [
|
|
|
8690
8708
|
] satisfies ParamDef[],
|
|
8691
8709
|
},
|
|
8692
8710
|
|
|
8711
|
+
// ---------------------------------------------------------------------------
|
|
8712
|
+
// T11373 — attention domain: Tier-2 scope-keyed working-memory jots (E2)
|
|
8713
|
+
// ---------------------------------------------------------------------------
|
|
8714
|
+
{
|
|
8715
|
+
gateway: 'mutate' as const,
|
|
8716
|
+
domain: 'attention',
|
|
8717
|
+
operation: 'add',
|
|
8718
|
+
description:
|
|
8719
|
+
'attention.add (mutate · alias jot) — record a scope-keyed working-memory ' +
|
|
8720
|
+
'jot. Session/agent identity + current task are auto-resolved from the ' +
|
|
8721
|
+
'environment (E0); the item keys to the narrowest scope (agent>task>epic>' +
|
|
8722
|
+
'saga>session>global). No session/agent/task flags by default.',
|
|
8723
|
+
tier: 1,
|
|
8724
|
+
idempotent: false,
|
|
8725
|
+
sessionRequired: false,
|
|
8726
|
+
requiredParams: ['content'],
|
|
8727
|
+
params: [
|
|
8728
|
+
{
|
|
8729
|
+
name: 'content',
|
|
8730
|
+
type: 'string' as const,
|
|
8731
|
+
required: true,
|
|
8732
|
+
description: 'The jot content',
|
|
8733
|
+
cli: { positional: true },
|
|
8734
|
+
},
|
|
8735
|
+
{
|
|
8736
|
+
name: 'tags',
|
|
8737
|
+
type: 'array' as const,
|
|
8738
|
+
required: false,
|
|
8739
|
+
description: 'Tags to attach (comma-separated on the CLI)',
|
|
8740
|
+
},
|
|
8741
|
+
{
|
|
8742
|
+
name: 'scope',
|
|
8743
|
+
type: 'string' as const,
|
|
8744
|
+
required: false,
|
|
8745
|
+
description:
|
|
8746
|
+
'Explicit scope escalation: agent|task|epic|saga|session|global (default: narrowest)',
|
|
8747
|
+
},
|
|
8748
|
+
{
|
|
8749
|
+
name: 'ttlSeconds',
|
|
8750
|
+
type: 'number' as const,
|
|
8751
|
+
required: false,
|
|
8752
|
+
description: 'Optional time-to-live in seconds (sets expires_at)',
|
|
8753
|
+
},
|
|
8754
|
+
] satisfies ParamDef[],
|
|
8755
|
+
},
|
|
8756
|
+
{
|
|
8757
|
+
gateway: 'query' as const,
|
|
8758
|
+
domain: 'attention',
|
|
8759
|
+
operation: 'list',
|
|
8760
|
+
description:
|
|
8761
|
+
'attention.list (query) — list open working-memory jots visible to the ' +
|
|
8762
|
+
'calling agent across its resolved scope chain. Optional --scope / --tag filters.',
|
|
8763
|
+
tier: 1,
|
|
8764
|
+
idempotent: true,
|
|
8765
|
+
sessionRequired: false,
|
|
8766
|
+
requiredParams: [],
|
|
8767
|
+
params: [
|
|
8768
|
+
{
|
|
8769
|
+
name: 'scope',
|
|
8770
|
+
type: 'string' as const,
|
|
8771
|
+
required: false,
|
|
8772
|
+
description: 'Restrict to one scope kind: agent|task|epic|saga|session|global',
|
|
8773
|
+
},
|
|
8774
|
+
{
|
|
8775
|
+
name: 'tags',
|
|
8776
|
+
type: 'array' as const,
|
|
8777
|
+
required: false,
|
|
8778
|
+
description: 'Filter by tags (contains-ALL; comma-separated on the CLI)',
|
|
8779
|
+
},
|
|
8780
|
+
{
|
|
8781
|
+
name: 'includeAll',
|
|
8782
|
+
type: 'boolean' as const,
|
|
8783
|
+
required: false,
|
|
8784
|
+
description: 'Include non-open (consolidated/discarded) items too. Default: false',
|
|
8785
|
+
},
|
|
8786
|
+
{
|
|
8787
|
+
name: 'limit',
|
|
8788
|
+
type: 'number' as const,
|
|
8789
|
+
required: false,
|
|
8790
|
+
description: 'Maximum rows. Default: 50',
|
|
8791
|
+
},
|
|
8792
|
+
] satisfies ParamDef[],
|
|
8793
|
+
},
|
|
8794
|
+
{
|
|
8795
|
+
gateway: 'query' as const,
|
|
8796
|
+
domain: 'attention',
|
|
8797
|
+
operation: 'show',
|
|
8798
|
+
description:
|
|
8799
|
+
'attention.show (query) — synonym for attention.list; emits the open ' +
|
|
8800
|
+
'working-memory jots for the calling agent within token budget.',
|
|
8801
|
+
tier: 1,
|
|
8802
|
+
idempotent: true,
|
|
8803
|
+
sessionRequired: false,
|
|
8804
|
+
requiredParams: [],
|
|
8805
|
+
params: [
|
|
8806
|
+
{
|
|
8807
|
+
name: 'scope',
|
|
8808
|
+
type: 'string' as const,
|
|
8809
|
+
required: false,
|
|
8810
|
+
description: 'Restrict to one scope kind: agent|task|epic|saga|session|global',
|
|
8811
|
+
},
|
|
8812
|
+
{
|
|
8813
|
+
name: 'tags',
|
|
8814
|
+
type: 'array' as const,
|
|
8815
|
+
required: false,
|
|
8816
|
+
description: 'Filter by tags (contains-ALL; comma-separated on the CLI)',
|
|
8817
|
+
},
|
|
8818
|
+
{
|
|
8819
|
+
name: 'includeAll',
|
|
8820
|
+
type: 'boolean' as const,
|
|
8821
|
+
required: false,
|
|
8822
|
+
description: 'Include non-open items too. Default: false',
|
|
8823
|
+
},
|
|
8824
|
+
{
|
|
8825
|
+
name: 'limit',
|
|
8826
|
+
type: 'number' as const,
|
|
8827
|
+
required: false,
|
|
8828
|
+
description: 'Maximum rows. Default: 50',
|
|
8829
|
+
},
|
|
8830
|
+
] satisfies ParamDef[],
|
|
8831
|
+
},
|
|
8832
|
+
|
|
8693
8833
|
// ---------------------------------------------------------------------------
|
|
8694
8834
|
// T10629 — tasks.context: bounded task-scoped context pack with token budget
|
|
8695
8835
|
// ---------------------------------------------------------------------------
|
package/src/exit-codes.ts
CHANGED
|
@@ -332,7 +332,7 @@ export const E_WORKFLOW_NOT_FOUND = 'E_WORKFLOW_NOT_FOUND' as const;
|
|
|
332
332
|
* retry. Emitted by {@link withWriteRetry} (in `@cleocode/core`'s
|
|
333
333
|
* `store/with-retry.ts`) when a SQLite write transaction fails 4 consecutive
|
|
334
334
|
* attempts with `SQLITE_BUSY: database is locked` despite the engine-level
|
|
335
|
-
* `busy_timeout=
|
|
335
|
+
* `busy_timeout=30000ms` pragma.
|
|
336
336
|
*
|
|
337
337
|
* **Recovery**: retry the operation after a brief delay (the contended writer
|
|
338
338
|
* usually commits within 1-2 seconds). If the error reoccurs reliably, suspect
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-drift guard for the FROZEN `gateway/rpc` v1.0 contract (T11449 AC).
|
|
3
|
+
*
|
|
4
|
+
* Pins the v1.0 protocol version, the exact `direction` discriminator set, and
|
|
5
|
+
* the protocol-level transport error codes. If a future change adds, removes,
|
|
6
|
+
* or renames a direction/error code without bumping to a new contract revision,
|
|
7
|
+
* these assertions fail — forcing an explicit, reviewed decision rather than
|
|
8
|
+
* silent drift that would break the CLI-RPC adapter and its clients.
|
|
9
|
+
*
|
|
10
|
+
* Also asserts the NDJSON frame round-trips through the Zod schemas, proving
|
|
11
|
+
* the framing discipline borrowed from `supervisor-ipc` (versioned, correlated
|
|
12
|
+
* envelope discriminated by `direction`, one frame per line) holds for the
|
|
13
|
+
* gateway-dispatch payload.
|
|
14
|
+
*
|
|
15
|
+
* @task T11449
|
|
16
|
+
* @epic T11254
|
|
17
|
+
* @saga T11243
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, it } from 'vitest';
|
|
21
|
+
import {
|
|
22
|
+
GATEWAY_RPC_CHANNEL_BASENAME,
|
|
23
|
+
GATEWAY_RPC_DIRECTIONS,
|
|
24
|
+
GATEWAY_RPC_ERROR_CODES,
|
|
25
|
+
GATEWAY_RPC_PROTOCOL_VERSION,
|
|
26
|
+
GatewayRpcFrameSchema,
|
|
27
|
+
isFrozenGatewayRpcVersion,
|
|
28
|
+
} from '../index.js';
|
|
29
|
+
|
|
30
|
+
describe('gateway/rpc v1.0 freeze guard (T11449)', () => {
|
|
31
|
+
it('pins the frozen protocol version', () => {
|
|
32
|
+
expect(GATEWAY_RPC_PROTOCOL_VERSION).toBe('1.0.0');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('pins the default channel basename', () => {
|
|
36
|
+
expect(GATEWAY_RPC_CHANNEL_BASENAME).toBe('cleo-gateway-rpc');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('pins the EXACT frozen direction set', () => {
|
|
40
|
+
expect([...GATEWAY_RPC_DIRECTIONS]).toEqual(['request', 'response', 'error']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('pins the EXACT frozen protocol-level error-code set', () => {
|
|
44
|
+
expect([...GATEWAY_RPC_ERROR_CODES]).toEqual([
|
|
45
|
+
'E_RPC_PARSE',
|
|
46
|
+
'E_RPC_BAD_VERSION',
|
|
47
|
+
'E_RPC_BAD_FRAME',
|
|
48
|
+
'E_RPC_INTERNAL',
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('isFrozenGatewayRpcVersion accepts only the frozen version', () => {
|
|
53
|
+
expect(isFrozenGatewayRpcVersion('1.0.0')).toBe(true);
|
|
54
|
+
expect(isFrozenGatewayRpcVersion('2.0.0')).toBe(false);
|
|
55
|
+
expect(isFrozenGatewayRpcVersion('0.9.0')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('gateway/rpc v1.0 NDJSON frame round-trip', () => {
|
|
60
|
+
it('parses a request frame wrapping a source:rpc DispatchRequest', () => {
|
|
61
|
+
const wire = {
|
|
62
|
+
protocol_version: '1.0.0',
|
|
63
|
+
id: 'abc',
|
|
64
|
+
direction: 'request',
|
|
65
|
+
request: {
|
|
66
|
+
gateway: 'query',
|
|
67
|
+
domain: 'tasks',
|
|
68
|
+
operation: 'show',
|
|
69
|
+
params: { id: 'T1' },
|
|
70
|
+
source: 'rpc',
|
|
71
|
+
requestId: 'abc',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const parsed = GatewayRpcFrameSchema.safeParse(wire);
|
|
75
|
+
expect(parsed.success).toBe(true);
|
|
76
|
+
if (parsed.success && parsed.data.direction === 'request') {
|
|
77
|
+
expect(parsed.data.request.source).toBe('rpc');
|
|
78
|
+
expect(parsed.data.request.operation).toBe('show');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('parses a response frame wrapping a LAFS DispatchResponse', () => {
|
|
83
|
+
const wire = {
|
|
84
|
+
protocol_version: '1.0.0',
|
|
85
|
+
id: 'abc',
|
|
86
|
+
direction: 'response',
|
|
87
|
+
response: {
|
|
88
|
+
meta: {
|
|
89
|
+
gateway: 'query',
|
|
90
|
+
domain: 'tasks',
|
|
91
|
+
operation: 'show',
|
|
92
|
+
timestamp: '2026-05-31T00:00:00.000Z',
|
|
93
|
+
duration_ms: 1,
|
|
94
|
+
source: 'rpc',
|
|
95
|
+
requestId: 'abc',
|
|
96
|
+
},
|
|
97
|
+
success: true,
|
|
98
|
+
data: { id: 'T1' },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
expect(GatewayRpcFrameSchema.safeParse(wire).success).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses a protocol-level error frame', () => {
|
|
105
|
+
const wire = {
|
|
106
|
+
protocol_version: '1.0.0',
|
|
107
|
+
id: '0',
|
|
108
|
+
direction: 'error',
|
|
109
|
+
error: { code: 'E_RPC_BAD_VERSION', message: 'unsupported protocol version' },
|
|
110
|
+
};
|
|
111
|
+
expect(GatewayRpcFrameSchema.safeParse(wire).success).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('rejects an unknown direction (proves the union is closed)', () => {
|
|
115
|
+
const wire = {
|
|
116
|
+
protocol_version: '1.0.0',
|
|
117
|
+
id: 'abc',
|
|
118
|
+
direction: 'teleport',
|
|
119
|
+
request: {},
|
|
120
|
+
};
|
|
121
|
+
expect(GatewayRpcFrameSchema.safeParse(wire).success).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('rejects a request frame whose source is NOT rpc-shaped enum member', () => {
|
|
125
|
+
const wire = {
|
|
126
|
+
protocol_version: '1.0.0',
|
|
127
|
+
id: 'abc',
|
|
128
|
+
direction: 'request',
|
|
129
|
+
request: {
|
|
130
|
+
gateway: 'query',
|
|
131
|
+
domain: 'tasks',
|
|
132
|
+
operation: 'show',
|
|
133
|
+
source: 'carrier-pigeon',
|
|
134
|
+
requestId: 'abc',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
expect(GatewayRpcFrameSchema.safeParse(wire).success).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gateway/rpc` v1.0 — FROZEN CLI-RPC NDJSON contract barrel.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the version constants and Zod frame schemas + inferred types for
|
|
5
|
+
* the CLI-RPC transport (`@cleocode/runtime/gateway/rpc`): a long-lived
|
|
6
|
+
* unix-socket RPC server that decodes NDJSON {@link GatewayRpcFrame}s into a
|
|
7
|
+
* `source: 'rpc'` gateway request and routes them through the gateway handler.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the `supervisor-ipc` contract structure (version.ts + messages.ts +
|
|
10
|
+
* this barrel + a freeze guard test) but carries gateway dispatch traffic, not
|
|
11
|
+
* supervisor process-control messages. The request/response PAYLOADS are owned
|
|
12
|
+
* by `@cleocode/contracts/gateway` (R3-T2 · T11446) and re-validated here at
|
|
13
|
+
* the untrusted RPC boundary.
|
|
14
|
+
*
|
|
15
|
+
* @task T11449
|
|
16
|
+
* @epic T11254
|
|
17
|
+
* @saga T11243
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
GatewayRpcDirection,
|
|
23
|
+
GatewayRpcError,
|
|
24
|
+
GatewayRpcErrorCode,
|
|
25
|
+
GatewayRpcErrorFrame,
|
|
26
|
+
GatewayRpcFrame,
|
|
27
|
+
GatewayRpcRequestFrame,
|
|
28
|
+
GatewayRpcResponseFrame,
|
|
29
|
+
} from './messages.js';
|
|
30
|
+
export {
|
|
31
|
+
GATEWAY_RPC_DIRECTIONS,
|
|
32
|
+
GATEWAY_RPC_ERROR_CODES,
|
|
33
|
+
GatewayRpcErrorFrameSchema,
|
|
34
|
+
GatewayRpcErrorSchema,
|
|
35
|
+
GatewayRpcFrameSchema,
|
|
36
|
+
GatewayRpcRequestFrameSchema,
|
|
37
|
+
GatewayRpcResponseFrameSchema,
|
|
38
|
+
isFrozenGatewayRpcVersion,
|
|
39
|
+
} from './messages.js';
|
|
40
|
+
export {
|
|
41
|
+
GATEWAY_RPC_CHANNEL_BASENAME,
|
|
42
|
+
GATEWAY_RPC_PROTOCOL_VERSION,
|
|
43
|
+
} from './version.js';
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gateway/rpc` v1.0 — FROZEN NDJSON message contract (Zod schemas).
|
|
3
|
+
*
|
|
4
|
+
* The CLI-RPC transport carries gateway dispatch traffic over a long-lived
|
|
5
|
+
* unix-domain socket. The wire format is newline-delimited JSON (NDJSON): one
|
|
6
|
+
* {@link GatewayRpcFrame} per line — exactly the framing discipline used by the
|
|
7
|
+
* FROZEN `supervisor-ipc` v1.0 contract, REUSED here (not edited): a versioned,
|
|
8
|
+
* correlation-id'd envelope discriminated by `direction`.
|
|
9
|
+
*
|
|
10
|
+
* The difference from supervisor-ipc is the PAYLOAD: this contract wraps the
|
|
11
|
+
* frozen gateway shapes ({@link dispatchRequestSchema} /
|
|
12
|
+
* {@link dispatchResponseSchema} from `@cleocode/contracts/gateway`) rather
|
|
13
|
+
* than supervisor process-control messages. So:
|
|
14
|
+
*
|
|
15
|
+
* ```jsonc
|
|
16
|
+
* // request frame (client → server)
|
|
17
|
+
* { "protocol_version": "1.0.0", "id": "abc", "direction": "request",
|
|
18
|
+
* "request": { "gateway": "query", "domain": "tasks", "operation": "show",
|
|
19
|
+
* "params": { "id": "T1" }, "source": "rpc",
|
|
20
|
+
* "requestId": "abc" } }
|
|
21
|
+
* // response frame (server → client)
|
|
22
|
+
* { "protocol_version": "1.0.0", "id": "abc", "direction": "response",
|
|
23
|
+
* "response": { "success": true, "data": {…}, "meta": {…} } }
|
|
24
|
+
* // error frame (server → client) — protocol-level (bad version / parse / unknown)
|
|
25
|
+
* { "protocol_version": "1.0.0", "id": "abc", "direction": "error",
|
|
26
|
+
* "error": { "code": "E_RPC_BAD_VERSION", "message": "…" } }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* FROZEN v1.0: the {@link GATEWAY_RPC_DIRECTIONS} set is pinned by the drift
|
|
30
|
+
* guard (`__tests__/freeze.test.ts`). Do not add/rename/remove directions in
|
|
31
|
+
* place — bump to a new versioned directory instead. The request/response
|
|
32
|
+
* PAYLOADS are owned by the gateway contract (R3-T2 · T11446) and re-validated
|
|
33
|
+
* here so an untrusted RPC boundary cannot inject a malformed dispatch request.
|
|
34
|
+
*
|
|
35
|
+
* @task T11449
|
|
36
|
+
* @epic T11254
|
|
37
|
+
* @saga T11243
|
|
38
|
+
* @packageDocumentation
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { z } from 'zod';
|
|
42
|
+
import { dispatchRequestSchema, dispatchResponseSchema } from '../../gateway.js';
|
|
43
|
+
import { GATEWAY_RPC_PROTOCOL_VERSION } from './version.js';
|
|
44
|
+
|
|
45
|
+
// ─── Protocol-level error payload ──────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A protocol-level (transport) error returned when a frame cannot even reach
|
|
49
|
+
* the gateway: malformed JSON, a wrong `protocol_version`, or a frame the
|
|
50
|
+
* server cannot route. Distinct from a {@link dispatchResponseSchema} `error`
|
|
51
|
+
* field, which is a DOMAIN error produced by the handler itself.
|
|
52
|
+
*/
|
|
53
|
+
export const GatewayRpcErrorSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
/** Machine-readable transport error code (e.g. `E_RPC_BAD_VERSION`). */
|
|
56
|
+
code: z.string().min(1),
|
|
57
|
+
/** Human-readable error message. */
|
|
58
|
+
message: z.string(),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
|
|
62
|
+
/** Protocol-level RPC error payload (inferred from {@link GatewayRpcErrorSchema}). */
|
|
63
|
+
export type GatewayRpcError = z.infer<typeof GatewayRpcErrorSchema>;
|
|
64
|
+
|
|
65
|
+
// ─── Request frame (client → server) ───────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A client → server request frame: a versioned, correlated wrapper around a
|
|
69
|
+
* single frozen {@link dispatchRequestSchema}. The `direction` discriminator +
|
|
70
|
+
* flattened `request` payload mirror the supervisor-ipc envelope shape.
|
|
71
|
+
*/
|
|
72
|
+
export const GatewayRpcRequestFrameSchema = z
|
|
73
|
+
.object({
|
|
74
|
+
/** Frozen protocol version; rejected if it differs from the server's. */
|
|
75
|
+
protocol_version: z.string(),
|
|
76
|
+
/** Correlation id echoed back on the matching response/error frame. */
|
|
77
|
+
id: z.string().min(1),
|
|
78
|
+
/** Direction discriminator. */
|
|
79
|
+
direction: z.literal('request'),
|
|
80
|
+
/** The gateway dispatch request (re-validated at the untrusted boundary). */
|
|
81
|
+
request: dispatchRequestSchema,
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
84
|
+
|
|
85
|
+
/** Request frame (inferred from {@link GatewayRpcRequestFrameSchema}). */
|
|
86
|
+
export type GatewayRpcRequestFrame = z.infer<typeof GatewayRpcRequestFrameSchema>;
|
|
87
|
+
|
|
88
|
+
// ─── Response frame (server → client) ──────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A server → client response frame: a versioned, correlated wrapper around a
|
|
92
|
+
* single frozen {@link dispatchResponseSchema} (the LAFS envelope the handler
|
|
93
|
+
* produced).
|
|
94
|
+
*/
|
|
95
|
+
export const GatewayRpcResponseFrameSchema = z
|
|
96
|
+
.object({
|
|
97
|
+
/** Frozen protocol version. */
|
|
98
|
+
protocol_version: z.string(),
|
|
99
|
+
/** Correlation id echoed from the originating request. */
|
|
100
|
+
id: z.string().min(1),
|
|
101
|
+
/** Direction discriminator. */
|
|
102
|
+
direction: z.literal('response'),
|
|
103
|
+
/** The gateway dispatch response (LAFS envelope). */
|
|
104
|
+
response: dispatchResponseSchema,
|
|
105
|
+
})
|
|
106
|
+
.strict();
|
|
107
|
+
|
|
108
|
+
/** Response frame (inferred from {@link GatewayRpcResponseFrameSchema}). */
|
|
109
|
+
export type GatewayRpcResponseFrame = z.infer<typeof GatewayRpcResponseFrameSchema>;
|
|
110
|
+
|
|
111
|
+
// ─── Error frame (server → client) ─────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A server → client protocol-level error frame, emitted when a request frame
|
|
115
|
+
* cannot be decoded or routed. Correlation `id` is best-effort (`'0'` when the
|
|
116
|
+
* frame was so malformed the id could not be recovered).
|
|
117
|
+
*/
|
|
118
|
+
export const GatewayRpcErrorFrameSchema = z
|
|
119
|
+
.object({
|
|
120
|
+
/** Frozen protocol version. */
|
|
121
|
+
protocol_version: z.string(),
|
|
122
|
+
/** Correlation id (best-effort; `'0'` when unrecoverable). */
|
|
123
|
+
id: z.string().min(1),
|
|
124
|
+
/** Direction discriminator. */
|
|
125
|
+
direction: z.literal('error'),
|
|
126
|
+
/** The protocol-level error. */
|
|
127
|
+
error: GatewayRpcErrorSchema,
|
|
128
|
+
})
|
|
129
|
+
.strict();
|
|
130
|
+
|
|
131
|
+
/** Error frame (inferred from {@link GatewayRpcErrorFrameSchema}). */
|
|
132
|
+
export type GatewayRpcErrorFrame = z.infer<typeof GatewayRpcErrorFrameSchema>;
|
|
133
|
+
|
|
134
|
+
// ─── Top-level frame union ─────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The top-level RPC frame: a versioned, correlated wrapper that is a request,
|
|
138
|
+
* a response, or a protocol-level error, discriminated by `direction`. One
|
|
139
|
+
* frame per NDJSON line.
|
|
140
|
+
*/
|
|
141
|
+
export const GatewayRpcFrameSchema = z.discriminatedUnion('direction', [
|
|
142
|
+
GatewayRpcRequestFrameSchema,
|
|
143
|
+
GatewayRpcResponseFrameSchema,
|
|
144
|
+
GatewayRpcErrorFrameSchema,
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
/** Any RPC frame (inferred from {@link GatewayRpcFrameSchema}). */
|
|
148
|
+
export type GatewayRpcFrame = z.infer<typeof GatewayRpcFrameSchema>;
|
|
149
|
+
|
|
150
|
+
// ─── Frozen direction-set guard ────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The FROZEN v1.0 `direction` discriminator values. The drift guard
|
|
154
|
+
* (`__tests__/freeze.test.ts`) pins this tuple; any addition/removal is a
|
|
155
|
+
* contract-breaking change requiring a new versioned directory.
|
|
156
|
+
*/
|
|
157
|
+
export const GATEWAY_RPC_DIRECTIONS = ['request', 'response', 'error'] as const;
|
|
158
|
+
|
|
159
|
+
/** A single frozen RPC frame direction. */
|
|
160
|
+
export type GatewayRpcDirection = (typeof GATEWAY_RPC_DIRECTIONS)[number];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The FROZEN v1.0 protocol-level transport error codes the adapter emits in an
|
|
164
|
+
* {@link GatewayRpcErrorFrameSchema}. Pinned by the drift guard.
|
|
165
|
+
*/
|
|
166
|
+
export const GATEWAY_RPC_ERROR_CODES = [
|
|
167
|
+
'E_RPC_PARSE',
|
|
168
|
+
'E_RPC_BAD_VERSION',
|
|
169
|
+
'E_RPC_BAD_FRAME',
|
|
170
|
+
'E_RPC_INTERNAL',
|
|
171
|
+
] as const;
|
|
172
|
+
|
|
173
|
+
/** A single frozen RPC protocol-level error code. */
|
|
174
|
+
export type GatewayRpcErrorCode = (typeof GATEWAY_RPC_ERROR_CODES)[number];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Type guard: is `v` the frozen RPC protocol version this contract pins?
|
|
178
|
+
*
|
|
179
|
+
* Used by the adapter's version-negotiation step to reject mismatched frames
|
|
180
|
+
* with an `E_RPC_BAD_VERSION` error frame before any gateway routing.
|
|
181
|
+
*
|
|
182
|
+
* @param v - The candidate `protocol_version` string from an inbound frame.
|
|
183
|
+
* @returns `true` iff `v` equals {@link GATEWAY_RPC_PROTOCOL_VERSION}.
|
|
184
|
+
*/
|
|
185
|
+
export function isFrozenGatewayRpcVersion(v: string): v is typeof GATEWAY_RPC_PROTOCOL_VERSION {
|
|
186
|
+
return v === GATEWAY_RPC_PROTOCOL_VERSION;
|
|
187
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FROZEN protocol version for the `gateway/rpc` v1.0 wire contract.
|
|
3
|
+
*
|
|
4
|
+
* The CLI-RPC transport adapter (`@cleocode/runtime/gateway/rpc`) and any RPC
|
|
5
|
+
* client MUST agree on this value. The wire format is newline-delimited JSON
|
|
6
|
+
* (NDJSON): one {@link GatewayRpcFrame} per line over a unix-domain socket —
|
|
7
|
+
* the same framing discipline as the FROZEN `supervisor-ipc` v1.0 contract
|
|
8
|
+
* (`packages/contracts/src/supervisor-ipc/`), but a SEPARATE message set: this
|
|
9
|
+
* carries `DispatchRequest`/`DispatchResponse` over the gateway, not supervisor
|
|
10
|
+
* process-control messages.
|
|
11
|
+
*
|
|
12
|
+
* Bump only via a new major contract revision in a new directory, never edit
|
|
13
|
+
* this value in place — see {@link GATEWAY_RPC_MESSAGE_KINDS} for the frozen
|
|
14
|
+
* v1.0 message set.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7464 | NDJSON framing}
|
|
17
|
+
* @task T11449
|
|
18
|
+
* @epic T11254
|
|
19
|
+
* @saga T11243
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The frozen `gateway/rpc` protocol version string.
|
|
25
|
+
*
|
|
26
|
+
* Both the TS drift test (`__tests__/freeze.test.ts`) and the adapter's
|
|
27
|
+
* version-negotiation handshake pin this exact value. A frame whose
|
|
28
|
+
* `protocol_version` differs from this is rejected with an `error` frame.
|
|
29
|
+
*/
|
|
30
|
+
export const GATEWAY_RPC_PROTOCOL_VERSION = '1.0.0' as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default base name for the RPC unix-socket channel.
|
|
34
|
+
*
|
|
35
|
+
* Callers resolve the concrete socket path under the CLEO home (e.g.
|
|
36
|
+
* `<cleoHome>/<basename>.sock`); the adapter itself takes the resolved path so
|
|
37
|
+
* the runtime stays free of any `@cleocode/paths` dependency. Mirrors
|
|
38
|
+
* {@link SUPERVISOR_IPC_CHANNEL_BASENAME} from the supervisor-ipc contract.
|
|
39
|
+
*/
|
|
40
|
+
export const GATEWAY_RPC_CHANNEL_BASENAME = 'cleo-gateway-rpc' as const;
|