macp-sdk-typescript 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +50 -20
- package/dist/client.d.ts +18 -5
- package/dist/client.js +24 -10
- package/dist/commitment-hash.d.ts +26 -0
- package/dist/commitment-hash.js +180 -0
- package/dist/constants.d.ts +29 -0
- package/dist/constants.js +50 -1
- package/dist/decision.js +4 -1
- package/dist/errors.d.ts +67 -6
- package/dist/errors.js +90 -47
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/policy.js +7 -3
- package/dist/projections/base.d.ts +14 -0
- package/dist/projections/base.js +18 -0
- package/dist/projections/decision.js +22 -15
- package/dist/projections/handoff.js +4 -4
- package/dist/projections/proposal.d.ts +13 -0
- package/dist/projections/proposal.js +19 -2
- package/dist/projections/quorum.js +1 -1
- package/dist/projections/task.d.ts +7 -0
- package/dist/projections/task.js +15 -4
- package/dist/types.d.ts +1 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +6 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watchers.js +8 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,83 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project uses [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.15.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.14.1...v0.15.0) (2026-10-17)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **commitment-hash:** RFC-MACP-0013 §6.1 verifyCommitmentHash + verify vectors 006–010 ([#170](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/170)) ([a39421c](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/a39421cfb2908731b1470ff787b0fffcacfd6299))
|
|
13
|
+
* **errors:** export DENY_* constants and KNOWN_DENY_CODES; keep message-less denials ([#169](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/169)) ([fef7335](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/fef733589427dd185fe445024e330c8c10d4c864)), closes [#168](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/168)
|
|
14
|
+
* **errors:** runtime 0.9.0 registry error codes and structured policy denials ([#164](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/164)) ([d37ad7f](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/d37ad7ff799e94b66271a08e7d18774b115d1c4c))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **validation:** cap base64url session ids at 128 bytes ([#167](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/167)) ([bb60588](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/bb60588f67edcb200e9f109491e3a262232a8a11))
|
|
20
|
+
|
|
21
|
+
## [0.14.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.14.0...v0.14.1) (2026-10-03)
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`TaskProjection.rejections: TaskRejectRecord[]`** (issue #151; a
|
|
26
|
+
cross-SDK parity audit against `macp-sdk-python`'s `TaskRejectRecord`,
|
|
27
|
+
`src/macp_sdk/task.py:53-58`). `TaskReject` now appends an unconditional
|
|
28
|
+
audit record — `taskId`, `assignee`, `reason?`, `sender` (from the
|
|
29
|
+
envelope, not the payload) — matching the convention already established
|
|
30
|
+
for `updates`/`completions`/`failures`. A `TaskReject` for an unknown
|
|
31
|
+
`taskId` still records a `rejections` entry (no crash, no silent drop) but
|
|
32
|
+
creates no `tasks` entry, unchanged from today. Deliberately does not fall
|
|
33
|
+
back `assignee` to the envelope sender the way Python's `p.assignee or
|
|
34
|
+
envelope.sender` does — this SDK's own sibling `TaskComplete`/`TaskFail`
|
|
35
|
+
handlers spread the payload's `assignee` as-is with no such fallback, and
|
|
36
|
+
intra-SDK consistency with that established pattern takes precedence here.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **`BaseProjection` phase no longer regresses once `Committed`, and
|
|
41
|
+
`DecisionProjection` no longer lets a replayed `Proposal` rewind phase
|
|
42
|
+
from `Voting` back to `Evaluation` after voting has begun** (issue #150,
|
|
43
|
+
issue #153; found by a cross-SDK parity audit against `macp-sdk-python`'s
|
|
44
|
+
`_set_phase()`, `src/macp_sdk/base_projection.py:289-313`). All five
|
|
45
|
+
built-in mode projections now route every phase write through a new
|
|
46
|
+
shared, protected `setPhase()` choke point on `BaseProjection` that
|
|
47
|
+
no-ops once `phase === 'Committed'`, instead of assigning `this.phase`
|
|
48
|
+
directly at each of the (previously) 12 call sites across
|
|
49
|
+
`decision.ts`/`proposal.ts`/`task.ts`/`quorum.ts`/`handoff.ts` — only
|
|
50
|
+
`decision.ts`'s own Vote-branch guard protected this before. RFC-MACP-0007
|
|
51
|
+
§5 rule 6 separately requires a runtime to reject any
|
|
52
|
+
`Proposal`/`Evaluation`/`Objection` after the first accepted `Vote`;
|
|
53
|
+
`DecisionProjection`'s `Proposal` handler now guards on the pre-
|
|
54
|
+
`Evaluation` phase value (`if (this.phase === 'Proposal')
|
|
55
|
+
this.setPhase('Evaluation')`) so a replayed/retried `Proposal` (same
|
|
56
|
+
`proposalId`, a distinct `messageId` that still passes
|
|
57
|
+
`applyEnvelope`'s `message_id` dedup gate) can no longer rewind `phase`
|
|
58
|
+
back to `'Evaluation'` once voting has begun. `BaseProjection.
|
|
59
|
+
applyEnvelope`'s own direct `this.phase = 'Committed'` assignment for a
|
|
60
|
+
`Commitment` envelope is deliberately left untouched — exact parity with
|
|
61
|
+
Python's own `_set_phase()` docstring, which documents the same bypass.
|
|
62
|
+
- **`buildDecisionPolicy` now rejects a `+Infinity` voting weight** (issue
|
|
63
|
+
#152; found by a cross-SDK parity audit against `macp-sdk-python`'s
|
|
64
|
+
`math.isfinite` guard, `src/macp_sdk/policy.py:186`). The per-weight
|
|
65
|
+
validation guard changed from `weight <= 0 || Number.isNaN(weight)` to
|
|
66
|
+
`weight <= 0 || !Number.isFinite(weight)` — `!Number.isFinite` is `true`
|
|
67
|
+
for `NaN`, `+Infinity`, and `-Infinity` alike, closing the `+Infinity` gap
|
|
68
|
+
the old `Number.isNaN`-only check missed (a `+Infinity` weight serializes
|
|
69
|
+
to JSON `null`, which is schema-invalid against
|
|
70
|
+
`decision-rules.schema.json`). `-Infinity`/`NaN` continue to throw,
|
|
71
|
+
unchanged.
|
|
72
|
+
- **`watchers.ts`'s 5 "stream ended before receiving a ..." sites now throw
|
|
73
|
+
`MacpTransportError` instead of a bare `Error`** (issue #154; parity with
|
|
74
|
+
`macp-sdk-python` PR #138). `ModeRegistryWatcher.nextChange()`,
|
|
75
|
+
`RootsWatcher.nextChange()`, `SignalWatcher.nextSignal()`,
|
|
76
|
+
`PolicyWatcher.nextChange()`, and `SessionLifecycleWatcher.nextChange()`
|
|
77
|
+
each threw a bare `Error` when their stream ended before yielding a value;
|
|
78
|
+
a caller's single `catch (e) { if (e instanceof MacpSdkError) ... }` now
|
|
79
|
+
covers a cleanly-ended watch stream alongside every other SDK failure.
|
|
80
|
+
Message strings are unchanged byte-for-byte; `error.code` remains
|
|
81
|
+
`undefined` on all 5 (locally raised, no gRPC status to attach).
|
|
82
|
+
- **parity:** re-vendor contract.json for contract_version 1.3.0 ([#156](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/156)) ([#157](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/157)) ([497949d](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/497949d2b29031d340f5d8cc2ef17ae34c946c42))
|
|
83
|
+
|
|
7
84
|
## [0.14.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.13.0...v0.14.0) (2026-10-01)
|
|
8
85
|
|
|
9
86
|
|
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@ await client.initialize();
|
|
|
43
43
|
const session = new DecisionSession(client);
|
|
44
44
|
await session.start({
|
|
45
45
|
intent: 'pick a deployment strategy',
|
|
46
|
-
|
|
46
|
+
// 'coordinator' (this client's own identity) sends propose()/commit() below with no
|
|
47
|
+
// sender override, so it must be a declared participant too.
|
|
48
|
+
participants: ['coordinator', 'alice', 'bob'],
|
|
47
49
|
ttlMs: 60_000,
|
|
48
50
|
});
|
|
49
51
|
|
|
@@ -108,7 +110,10 @@ Structured decision with proposals, evaluations, objections, and votes.
|
|
|
108
110
|
import { DecisionSession } from 'macp-sdk-typescript';
|
|
109
111
|
|
|
110
112
|
const session = new DecisionSession(client);
|
|
111
|
-
|
|
113
|
+
// 'coordinator' (this client's own identity) sends propose()/vote()/commit() below,
|
|
114
|
+
// so it must be a declared participant — Decision mode's default governance policy
|
|
115
|
+
// rejects a mode action from a non-participant sender with FORBIDDEN.
|
|
116
|
+
await session.start({ intent: '...', participants: ['coordinator', 'alice'], ttlMs: 60_000 });
|
|
112
117
|
await session.propose({ proposalId: 'p1', option: 'A', rationale: '...' });
|
|
113
118
|
await session.evaluate({ proposalId: 'p1', recommendation: 'approve', confidence: 0.9 });
|
|
114
119
|
await session.raiseObjection({ proposalId: 'p1', reason: 'risk', severity: 'high' });
|
|
@@ -118,7 +123,7 @@ await session.commit({ action: 'decided', authorityScope: 'team', reason: '...'
|
|
|
118
123
|
// Projection queries
|
|
119
124
|
session.projection.voteTotals(); // { p1: 1 }
|
|
120
125
|
session.projection.majorityWinner(); // 'p1'
|
|
121
|
-
session.projection.hasBlockingObjection('p1'); //
|
|
126
|
+
session.projection.hasBlockingObjection('p1'); // false (only severity 'critical' blocks)
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
### Proposal Mode
|
|
@@ -126,13 +131,17 @@ session.projection.hasBlockingObjection('p1'); // true (severity: high)
|
|
|
126
131
|
Proposal and counterproposal negotiation.
|
|
127
132
|
|
|
128
133
|
```typescript
|
|
129
|
-
import { ProposalSession } from 'macp-sdk-typescript';
|
|
134
|
+
import { Auth, ProposalSession } from 'macp-sdk-typescript';
|
|
130
135
|
|
|
131
136
|
const session = new ProposalSession(client);
|
|
132
|
-
|
|
137
|
+
// 'coordinator' sends propose() below, so it must be a declared participant (same
|
|
138
|
+
// FORBIDDEN rule as Decision mode). Proposal mode's default policy also requires
|
|
139
|
+
// *every* participant to accept() before commit() — not just the non-proposing ones.
|
|
140
|
+
await session.start({ intent: '...', participants: ['coordinator', 'bob'], ttlMs: 60_000 });
|
|
133
141
|
await session.propose({ proposalId: 'p1', title: 'Plan A', summary: '...' });
|
|
134
142
|
await session.counterPropose({ proposalId: 'p2', supersedesProposalId: 'p1', title: 'Plan B' });
|
|
135
143
|
await session.accept({ proposalId: 'p2', reason: 'better' });
|
|
144
|
+
await session.accept({ proposalId: 'p2', reason: 'bob agrees', sender: 'bob', auth: Auth.devAgent('bob') });
|
|
136
145
|
await session.commit({ action: 'proposal.accepted', authorityScope: 'team', reason: '...' });
|
|
137
146
|
|
|
138
147
|
// Projection queries
|
|
@@ -146,14 +155,18 @@ session.projection.isTerminallyRejected('p1'); // false
|
|
|
146
155
|
Bounded task delegation.
|
|
147
156
|
|
|
148
157
|
```typescript
|
|
149
|
-
import { TaskSession } from 'macp-sdk-typescript';
|
|
158
|
+
import { Auth, TaskSession } from 'macp-sdk-typescript';
|
|
150
159
|
|
|
151
160
|
const session = new TaskSession(client);
|
|
152
161
|
await session.start({ intent: '...', participants: ['worker'], ttlMs: 120_000 });
|
|
153
162
|
await session.requestTask({ taskId: 't1', title: 'Build feature', instructions: '...' });
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
// The coordinator requests and commits, but every task-side action must be sent by the
|
|
164
|
+
// assignee itself: Task mode's default policy rejects an acceptTask/updateTask/
|
|
165
|
+
// completeTask from any other sender with FORBIDDEN.
|
|
166
|
+
const asWorker = { sender: 'worker', auth: Auth.devAgent('worker') };
|
|
167
|
+
await session.acceptTask({ taskId: 't1', assignee: 'worker', ...asWorker });
|
|
168
|
+
await session.updateTask({ taskId: 't1', status: 'working', progress: 0.5, message: 'halfway', ...asWorker });
|
|
169
|
+
await session.completeTask({ taskId: 't1', assignee: 'worker', summary: 'done', ...asWorker });
|
|
157
170
|
await session.commit({ action: 'task.completed', authorityScope: 'lead', reason: '...' });
|
|
158
171
|
|
|
159
172
|
// Projection queries
|
|
@@ -167,13 +180,22 @@ session.projection.activeTasks(); // []
|
|
|
167
180
|
Responsibility transfer between participants.
|
|
168
181
|
|
|
169
182
|
```typescript
|
|
170
|
-
import { HandoffSession } from 'macp-sdk-typescript';
|
|
183
|
+
import { Auth, HandoffSession } from 'macp-sdk-typescript';
|
|
171
184
|
|
|
172
185
|
const session = new HandoffSession(client);
|
|
173
|
-
|
|
186
|
+
// Handoff's SessionStart itself requires >= 2 participants, AND the initiator
|
|
187
|
+
// ('coordinator', this client's own identity) must be one of them.
|
|
188
|
+
await session.start({ intent: '...', participants: ['coordinator', 'bob'], ttlMs: 60_000 });
|
|
174
189
|
await session.offer({ handoffId: 'h1', targetParticipant: 'bob', scope: 'frontend' });
|
|
175
190
|
await session.addContext({ handoffId: 'h1', contentType: 'application/json', context: buf });
|
|
176
|
-
|
|
191
|
+
// Only the offer's targetParticipant may accept — sender must be 'bob', not the
|
|
192
|
+
// session's own coordinator identity.
|
|
193
|
+
await session.acceptHandoff({
|
|
194
|
+
handoffId: 'h1',
|
|
195
|
+
acceptedBy: 'bob',
|
|
196
|
+
sender: 'bob',
|
|
197
|
+
auth: Auth.devAgent('bob'),
|
|
198
|
+
});
|
|
177
199
|
await session.commit({ action: 'handoff.accepted', authorityScope: 'team', reason: '...' });
|
|
178
200
|
|
|
179
201
|
// Projection queries
|
|
@@ -186,19 +208,24 @@ session.projection.pendingHandoffs(); // []
|
|
|
186
208
|
Threshold-based approval voting.
|
|
187
209
|
|
|
188
210
|
```typescript
|
|
189
|
-
import { QuorumSession } from 'macp-sdk-typescript';
|
|
211
|
+
import { Auth, QuorumSession } from 'macp-sdk-typescript';
|
|
190
212
|
|
|
191
213
|
const session = new QuorumSession(client);
|
|
192
214
|
await session.start({ intent: '...', participants: ['alice', 'bob', 'carol'], ttlMs: 60_000 });
|
|
193
215
|
await session.requestApproval({ requestId: 'r1', action: 'deploy', summary: '...', requiredApprovals: 2 });
|
|
194
|
-
|
|
216
|
+
// Ballots must come from a declared participant ('coordinator' is not one here), so each
|
|
217
|
+
// approve() needs its own sender. commit() additionally requires the threshold to be met —
|
|
218
|
+
// with requiredApprovals: 2, a single approval makes the runtime reject the Commitment
|
|
219
|
+
// with INVALID_ENVELOPE.
|
|
220
|
+
await session.approve({ requestId: 'r1', reason: 'ok', sender: 'alice', auth: Auth.devAgent('alice') });
|
|
221
|
+
await session.approve({ requestId: 'r1', reason: 'ok', sender: 'bob', auth: Auth.devAgent('bob') });
|
|
195
222
|
await session.commit({ action: 'quorum.approved', authorityScope: 'ops', reason: '...' });
|
|
196
223
|
|
|
197
224
|
// Projection queries
|
|
198
|
-
session.projection.hasQuorum('r1'); // true
|
|
199
|
-
session.projection.approvalCount('r1'); //
|
|
200
|
-
session.projection.remainingVotesNeeded('r1'); //
|
|
201
|
-
session.projection.votedSenders('r1'); //
|
|
225
|
+
session.projection.hasQuorum('r1'); // true
|
|
226
|
+
session.projection.approvalCount('r1'); // 2
|
|
227
|
+
session.projection.remainingVotesNeeded('r1'); // 0
|
|
228
|
+
session.projection.votedSenders('r1'); // ['alice', 'bob']
|
|
202
229
|
```
|
|
203
230
|
|
|
204
231
|
## Agent Framework
|
|
@@ -412,12 +439,15 @@ npm run format # Prettier
|
|
|
412
439
|
npm test # Run unit + conformance tests
|
|
413
440
|
npm run test:watch # Watch mode
|
|
414
441
|
npm run test:coverage # With coverage (CI enforces thresholds from vitest.config.ts)
|
|
415
|
-
npm run test:integration # Integration tests (requires Docker runtime
|
|
442
|
+
npm run test:integration # Integration tests (requires Docker runtime)
|
|
416
443
|
```
|
|
417
444
|
|
|
418
445
|
CI (GitHub Actions) runs type-check, lint, format check, the coverage-gated
|
|
419
446
|
test suite, and the build on Node 22 and 24 for every push and pull
|
|
420
|
-
request, and posts a coverage summary comment on PRs.
|
|
447
|
+
request, and posts a coverage summary comment on PRs. A separate workflow
|
|
448
|
+
(`.github/workflows/integration.yml`) runs `npm run test:integration` against a live
|
|
449
|
+
runtime service container on every pull request too — an example that no longer runs
|
|
450
|
+
cleanly fails that workflow, not just a local check.
|
|
421
451
|
|
|
422
452
|
### Integration Tests
|
|
423
453
|
|
package/dist/client.d.ts
CHANGED
|
@@ -29,10 +29,21 @@ interface MacpClientOptions {
|
|
|
29
29
|
* unknown codes. Used to populate {@link MacpTransportError.code}.
|
|
30
30
|
*/
|
|
31
31
|
export declare function grpcStatusName(code: unknown): string | undefined;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* An inline `MACPError` frame on a `StreamSession` stream. Since runtime 0.9.0
|
|
34
|
+
* `code` is the error-codes registry code (`INVALID_ENVELOPE`,
|
|
35
|
+
* `POLICY_DENIED`, ...) — before, it held the status message text — and
|
|
36
|
+
* `messageId`/`details` identify the rejected envelope and carry the same
|
|
37
|
+
* structured details a unary `Ack` does (decode with `parseErrorDetails`).
|
|
38
|
+
*/
|
|
39
|
+
export interface InlineStreamError {
|
|
33
40
|
code?: string;
|
|
34
41
|
message?: string;
|
|
35
|
-
|
|
42
|
+
sessionId?: string;
|
|
43
|
+
messageId?: string;
|
|
44
|
+
details?: Buffer;
|
|
45
|
+
}
|
|
46
|
+
export type InlineErrorCallback = (error: InlineStreamError) => void;
|
|
36
47
|
export declare class MacpStream {
|
|
37
48
|
private readonly call;
|
|
38
49
|
private readonly queue;
|
|
@@ -56,9 +67,11 @@ export declare class MacpStream {
|
|
|
56
67
|
* delivery, redeliveries included; see `GrpcTransportAdapter.lastSequence`
|
|
57
68
|
* for a cursor that tracks the ordinal correctly.
|
|
58
69
|
* Ordinals are stable across log compaction and runtime restart. Resuming
|
|
59
|
-
* below a compacted base
|
|
60
|
-
*
|
|
61
|
-
*
|
|
70
|
+
* below a compacted base is refused rather than silently skipping history
|
|
71
|
+
* (runtime ≥ 0.5.0; older runtimes compared inclusively against a raw log
|
|
72
|
+
* index): since runtime 0.9.0 as an inline `INVALID_ENVELOPE` error frame
|
|
73
|
+
* (see `onInlineError`) with the stream left open; on 0.5.0–0.8.x it ended
|
|
74
|
+
* the stream with `FAILED_PRECONDITION` (`MacpTransportError.code`).
|
|
62
75
|
*/
|
|
63
76
|
sendSubscribe(sessionId: string, afterSequence?: number): Promise<void>;
|
|
64
77
|
responses(): AsyncGenerator<Envelope, void, void>;
|
package/dist/client.js
CHANGED
|
@@ -156,13 +156,17 @@ class MacpStream {
|
|
|
156
156
|
}
|
|
157
157
|
else if (chunk?.error) {
|
|
158
158
|
// Inline application-level error — stream stays open (core.proto).
|
|
159
|
-
logging_1.logger.warn('inline stream error', {
|
|
159
|
+
logging_1.logger.warn('inline stream error', {
|
|
160
|
+
code: chunk.error.code,
|
|
161
|
+
message: chunk.error.message,
|
|
162
|
+
messageId: chunk.error.messageId,
|
|
163
|
+
});
|
|
160
164
|
for (const cb of this.inlineErrorCallbacks)
|
|
161
165
|
cb(chunk.error);
|
|
162
166
|
}
|
|
163
167
|
});
|
|
164
168
|
call.on('error', (error) => {
|
|
165
|
-
this.queue.push(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code)));
|
|
169
|
+
this.queue.push(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code), (0, errors_1.macpErrorCodeFromMetadata)(grpcMetadataEntries(error.metadata))));
|
|
166
170
|
});
|
|
167
171
|
call.on('end', () => {
|
|
168
172
|
this.queue.push(STREAM_END);
|
|
@@ -198,9 +202,11 @@ class MacpStream {
|
|
|
198
202
|
* delivery, redeliveries included; see `GrpcTransportAdapter.lastSequence`
|
|
199
203
|
* for a cursor that tracks the ordinal correctly.
|
|
200
204
|
* Ordinals are stable across log compaction and runtime restart. Resuming
|
|
201
|
-
* below a compacted base
|
|
202
|
-
*
|
|
203
|
-
*
|
|
205
|
+
* below a compacted base is refused rather than silently skipping history
|
|
206
|
+
* (runtime ≥ 0.5.0; older runtimes compared inclusively against a raw log
|
|
207
|
+
* index): since runtime 0.9.0 as an inline `INVALID_ENVELOPE` error frame
|
|
208
|
+
* (see `onInlineError`) with the stream left open; on 0.5.0–0.8.x it ended
|
|
209
|
+
* the stream with `FAILED_PRECONDITION` (`MacpTransportError.code`).
|
|
204
210
|
*/
|
|
205
211
|
sendSubscribe(sessionId, afterSequence = 0) {
|
|
206
212
|
if (this.closed)
|
|
@@ -318,17 +324,23 @@ class MacpClient {
|
|
|
318
324
|
const resolved = deadlineMs ?? this.defaultDeadlineMs;
|
|
319
325
|
return resolved ? new Date(Date.now() + resolved) : undefined;
|
|
320
326
|
}
|
|
321
|
-
unary(method, request, auth, deadlineMs, ackStatuses) {
|
|
327
|
+
unary(method, request, auth, deadlineMs, ackStatuses, registryCodeAsAck = false) {
|
|
322
328
|
return new Promise((resolve, reject) => {
|
|
323
329
|
const callback = (error, response) => {
|
|
324
330
|
if (error) {
|
|
325
331
|
const statusName = grpcStatusName(error.code);
|
|
326
|
-
const
|
|
332
|
+
const entries = grpcMetadataEntries(error.metadata);
|
|
333
|
+
// Runtime ≥ 0.9.0 names the registry code in `macp-error-code`
|
|
334
|
+
// metadata; several codes share one gRPC status, so when the call
|
|
335
|
+
// opts in, that code wins over the status-name map (which remains
|
|
336
|
+
// the fallback for older runtimes that send no such key).
|
|
337
|
+
const macpErrorCode = (0, errors_1.macpErrorCodeFromMetadata)(entries);
|
|
338
|
+
const mappedCode = (registryCodeAsAck ? macpErrorCode : undefined) ?? (statusName ? ackStatuses?.[statusName] : undefined);
|
|
327
339
|
if (mappedCode) {
|
|
328
|
-
reject(new errors_1.MacpAckError({ ok: false, error: { code: mappedCode, message: error.details || error.message } },
|
|
340
|
+
reject(new errors_1.MacpAckError({ ok: false, error: { code: mappedCode, message: error.details || error.message } }, entries));
|
|
329
341
|
}
|
|
330
342
|
else {
|
|
331
|
-
reject(new errors_1.MacpTransportError(error.details || error.message, statusName));
|
|
343
|
+
reject(new errors_1.MacpTransportError(error.details || error.message, statusName, macpErrorCode));
|
|
332
344
|
}
|
|
333
345
|
}
|
|
334
346
|
else {
|
|
@@ -375,7 +387,9 @@ class MacpClient {
|
|
|
375
387
|
// gRPC ALREADY_EXISTS/FAILED_PRECONDITION on Send are application-level
|
|
376
388
|
// rejections the runtime couldn't express as a normal NACK (issue #107);
|
|
377
389
|
// surface them as MacpAckError, matching macp-sdk-python (client.py:442-452).
|
|
378
|
-
|
|
390
|
+
// That status map is the pre-0.9.0 fallback: a `macp-error-code` key
|
|
391
|
+
// (runtime ≥ 0.9.0) supplies the exact registry code instead.
|
|
392
|
+
{ ALREADY_EXISTS: constants_1.SESSION_ALREADY_EXISTS, FAILED_PRECONDITION: constants_1.POLICY_DENIED }, true);
|
|
379
393
|
const ack = response.ack;
|
|
380
394
|
// Duplicate acks are success — the message was already accepted
|
|
381
395
|
if (ack?.duplicate)
|
|
@@ -69,3 +69,29 @@ export declare function commitmentHash(payload: CommitmentPayload): string;
|
|
|
69
69
|
* `fullmatch` semantics for this pattern.
|
|
70
70
|
*/
|
|
71
71
|
export declare function isCanonicalCommitmentHash(value: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Result of {@link verifyCommitmentHash} (RFC-MACP-0013 §6.1). `cannot_verify`
|
|
74
|
+
* is a verdict on the payload encoding — never on the claim.
|
|
75
|
+
*/
|
|
76
|
+
export type VerifyOutcome = 'match' | 'mismatch' | 'cannot_verify';
|
|
77
|
+
/**
|
|
78
|
+
* Verifies a claimed commitment hash against a `CommitmentPayload`'s Protobuf
|
|
79
|
+
* wire encoding (RFC-MACP-0013 §6.1), returning exactly one of:
|
|
80
|
+
*
|
|
81
|
+
* - `'match'` — the claim equals the canonical hash of the decoded payload;
|
|
82
|
+
* - `'mismatch'` — it does not, including any claim not in canonical form
|
|
83
|
+
* (`SHA256:` prefix, uppercase hex, no prefix, surrounding whitespace):
|
|
84
|
+
* the comparison is exact, never normalized;
|
|
85
|
+
* - `'cannot_verify'` — the encoding is outside what the
|
|
86
|
+
* `macp-commitment-hash/1` label covers: an unknown field in the payload or
|
|
87
|
+
* in `supersedes` (§5), or an unparseable encoding — truncated, a known
|
|
88
|
+
* field with the wrong wire type, invalid UTF-8 in a string, a group, or
|
|
89
|
+
* field number 0 (§6.1 rule 1).
|
|
90
|
+
*
|
|
91
|
+
* Takes the wire bytes, not a decoded object, so unknown fields stay
|
|
92
|
+
* observable. A known field repeated on the wire follows Protobuf merge
|
|
93
|
+
* semantics (last scalar wins, `supersedes` merges), and a zero-length
|
|
94
|
+
* encoding is the all-default payload, which is hashable. Never throws: a
|
|
95
|
+
* non-byte `wire` is `'cannot_verify'`, a non-string claim `'mismatch'`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function verifyCommitmentHash(wire: Uint8Array, claimedHash: string): VerifyOutcome;
|
package/dist/commitment-hash.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.canonicalizeCommitmentPayload = canonicalizeCommitmentPayload;
|
|
4
4
|
exports.commitmentHash = commitmentHash;
|
|
5
5
|
exports.isCanonicalCommitmentHash = isCanonicalCommitmentHash;
|
|
6
|
+
exports.verifyCommitmentHash = verifyCommitmentHash;
|
|
6
7
|
/**
|
|
7
8
|
* RFC-MACP-0013 canonical commitment hashing.
|
|
8
9
|
*
|
|
@@ -230,3 +231,182 @@ function commitmentHash(payload) {
|
|
|
230
231
|
function isCanonicalCommitmentHash(value) {
|
|
231
232
|
return /^sha256:[0-9a-f]{64}$/.test(value);
|
|
232
233
|
}
|
|
234
|
+
/** Thrown internally by the wire reader; never escapes this module. */
|
|
235
|
+
class CannotVerify extends Error {
|
|
236
|
+
}
|
|
237
|
+
// `ignoreBOM: true` keeps a leading U+FEFF in a string field (the default
|
|
238
|
+
// strips it, which would hash different bytes than the sender's); `fatal`
|
|
239
|
+
// makes invalid UTF-8 throw, which RFC-MACP-0013 §6.1 rule 1 requires to be
|
|
240
|
+
// cannot-verify rather than U+FFFD-substituted.
|
|
241
|
+
const STRICT_UTF8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
242
|
+
/**
|
|
243
|
+
* Minimal strict Protobuf wire reader for the two message types the
|
|
244
|
+
* `macp-commitment-hash/1` label covers. Hand-written rather than protobufjs
|
|
245
|
+
* because protobufjs silently skips unknown fields, and RFC-MACP-0013 §5
|
|
246
|
+
* requires an unknown field to be cannot-verify, never dropped.
|
|
247
|
+
*/
|
|
248
|
+
class WireReader {
|
|
249
|
+
buf;
|
|
250
|
+
pos = 0;
|
|
251
|
+
constructor(buf) {
|
|
252
|
+
this.buf = buf;
|
|
253
|
+
}
|
|
254
|
+
get done() {
|
|
255
|
+
return this.pos >= this.buf.length;
|
|
256
|
+
}
|
|
257
|
+
/** Varint as a Number. Exact up to 2^53; larger values are only ever compared against small bounds. */
|
|
258
|
+
varint() {
|
|
259
|
+
let result = 0;
|
|
260
|
+
let scale = 1;
|
|
261
|
+
for (let i = 0; i < 10; i++) {
|
|
262
|
+
if (this.pos >= this.buf.length)
|
|
263
|
+
throw new CannotVerify('truncated varint');
|
|
264
|
+
const byte = this.buf[this.pos++];
|
|
265
|
+
result += (byte & 0x7f) * scale;
|
|
266
|
+
if ((byte & 0x80) === 0)
|
|
267
|
+
return result;
|
|
268
|
+
scale *= 128;
|
|
269
|
+
}
|
|
270
|
+
throw new CannotVerify('varint longer than 10 bytes');
|
|
271
|
+
}
|
|
272
|
+
/** Reads a varint and reports only whether it is non-zero (proto3 bool). */
|
|
273
|
+
nonZeroVarint() {
|
|
274
|
+
return this.varint() !== 0;
|
|
275
|
+
}
|
|
276
|
+
bytes() {
|
|
277
|
+
const len = this.varint();
|
|
278
|
+
if (len > this.buf.length - this.pos)
|
|
279
|
+
throw new CannotVerify('truncated length-delimited field');
|
|
280
|
+
const out = this.buf.subarray(this.pos, this.pos + len);
|
|
281
|
+
this.pos += len;
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
string() {
|
|
285
|
+
try {
|
|
286
|
+
return STRICT_UTF8.decode(this.bytes());
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err instanceof CannotVerify)
|
|
290
|
+
throw err;
|
|
291
|
+
throw new CannotVerify('invalid UTF-8');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Field number and wire type of the next tag; field 0, groups, and wire types 6/7 are unparseable. */
|
|
295
|
+
tag() {
|
|
296
|
+
const key = this.varint();
|
|
297
|
+
if (key > 0xffffffff)
|
|
298
|
+
throw new CannotVerify('tag out of range');
|
|
299
|
+
const field = Math.floor(key / 8);
|
|
300
|
+
const wireType = key % 8;
|
|
301
|
+
if (field === 0)
|
|
302
|
+
throw new CannotVerify('field number 0');
|
|
303
|
+
if (wireType === 3 || wireType === 4)
|
|
304
|
+
throw new CannotVerify('group');
|
|
305
|
+
if (wireType > 5)
|
|
306
|
+
throw new CannotVerify(`invalid wire type ${wireType}`);
|
|
307
|
+
return { field, wireType };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const WIRE_VARINT = 0;
|
|
311
|
+
const WIRE_LEN = 2;
|
|
312
|
+
// Field numbers of the frozen set (RFC-MACP-0013 §5), in proto order.
|
|
313
|
+
const PAYLOAD_STRING_FIELDS = {
|
|
314
|
+
1: 'commitmentId',
|
|
315
|
+
2: 'action',
|
|
316
|
+
3: 'authorityScope',
|
|
317
|
+
4: 'reason',
|
|
318
|
+
5: 'modeVersion',
|
|
319
|
+
6: 'policyVersion',
|
|
320
|
+
7: 'configurationVersion',
|
|
321
|
+
};
|
|
322
|
+
const FIELD_OUTCOME_POSITIVE = 8;
|
|
323
|
+
const FIELD_SUPERSEDES = 9;
|
|
324
|
+
const REF_STRING_FIELDS = { 1: 'sessionId', 2: 'commitmentHash' };
|
|
325
|
+
function expectWireType(actual, expected) {
|
|
326
|
+
if (actual !== expected)
|
|
327
|
+
throw new CannotVerify('known field with the wrong wire type');
|
|
328
|
+
}
|
|
329
|
+
/** Merges one `CommitmentRef` occurrence into `ref` (Protobuf message merge: present sub-fields overwrite). */
|
|
330
|
+
function mergeCommitmentRef(wire, ref) {
|
|
331
|
+
const r = new WireReader(wire);
|
|
332
|
+
while (!r.done) {
|
|
333
|
+
const { field, wireType } = r.tag();
|
|
334
|
+
const name = REF_STRING_FIELDS[field];
|
|
335
|
+
// `supersedes` carries exactly two fields (§5): anything else is outside the label.
|
|
336
|
+
if (name === undefined)
|
|
337
|
+
throw new CannotVerify(`unknown CommitmentRef field ${field}`);
|
|
338
|
+
expectWireType(wireType, WIRE_LEN);
|
|
339
|
+
ref[name] = r.string();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** Strictly decodes a `CommitmentPayload` wire encoding; throws `CannotVerify` on anything §5/§6.1 excludes. */
|
|
343
|
+
function decodeCommitmentPayloadStrict(wire) {
|
|
344
|
+
// Proto3 defaults: a zero-length encoding is the all-default payload (§6.1 rule 3).
|
|
345
|
+
const payload = {
|
|
346
|
+
commitmentId: '',
|
|
347
|
+
action: '',
|
|
348
|
+
authorityScope: '',
|
|
349
|
+
reason: '',
|
|
350
|
+
modeVersion: '',
|
|
351
|
+
policyVersion: '',
|
|
352
|
+
configurationVersion: '',
|
|
353
|
+
outcomePositive: false,
|
|
354
|
+
};
|
|
355
|
+
const r = new WireReader(wire);
|
|
356
|
+
while (!r.done) {
|
|
357
|
+
const { field, wireType } = r.tag();
|
|
358
|
+
const name = PAYLOAD_STRING_FIELDS[field];
|
|
359
|
+
if (name !== undefined) {
|
|
360
|
+
expectWireType(wireType, WIRE_LEN);
|
|
361
|
+
payload[name] = r.string(); // last value wins (§6.1 rule 2)
|
|
362
|
+
}
|
|
363
|
+
else if (field === FIELD_OUTCOME_POSITIVE) {
|
|
364
|
+
expectWireType(wireType, WIRE_VARINT);
|
|
365
|
+
payload.outcomePositive = r.nonZeroVarint();
|
|
366
|
+
}
|
|
367
|
+
else if (field === FIELD_SUPERSEDES) {
|
|
368
|
+
expectWireType(wireType, WIRE_LEN);
|
|
369
|
+
// Present even when empty; repeated occurrences merge (§6.1 rule 2).
|
|
370
|
+
payload.supersedes ??= { sessionId: '', commitmentHash: '' };
|
|
371
|
+
mergeCommitmentRef(r.bytes(), payload.supersedes);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
throw new CannotVerify(`field ${field} is outside the macp-commitment-hash/1 frozen set`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return payload;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Verifies a claimed commitment hash against a `CommitmentPayload`'s Protobuf
|
|
381
|
+
* wire encoding (RFC-MACP-0013 §6.1), returning exactly one of:
|
|
382
|
+
*
|
|
383
|
+
* - `'match'` — the claim equals the canonical hash of the decoded payload;
|
|
384
|
+
* - `'mismatch'` — it does not, including any claim not in canonical form
|
|
385
|
+
* (`SHA256:` prefix, uppercase hex, no prefix, surrounding whitespace):
|
|
386
|
+
* the comparison is exact, never normalized;
|
|
387
|
+
* - `'cannot_verify'` — the encoding is outside what the
|
|
388
|
+
* `macp-commitment-hash/1` label covers: an unknown field in the payload or
|
|
389
|
+
* in `supersedes` (§5), or an unparseable encoding — truncated, a known
|
|
390
|
+
* field with the wrong wire type, invalid UTF-8 in a string, a group, or
|
|
391
|
+
* field number 0 (§6.1 rule 1).
|
|
392
|
+
*
|
|
393
|
+
* Takes the wire bytes, not a decoded object, so unknown fields stay
|
|
394
|
+
* observable. A known field repeated on the wire follows Protobuf merge
|
|
395
|
+
* semantics (last scalar wins, `supersedes` merges), and a zero-length
|
|
396
|
+
* encoding is the all-default payload, which is hashable. Never throws: a
|
|
397
|
+
* non-byte `wire` is `'cannot_verify'`, a non-string claim `'mismatch'`.
|
|
398
|
+
*/
|
|
399
|
+
function verifyCommitmentHash(wire, claimedHash) {
|
|
400
|
+
if (!(wire instanceof Uint8Array))
|
|
401
|
+
return 'cannot_verify';
|
|
402
|
+
let payload;
|
|
403
|
+
try {
|
|
404
|
+
payload = decodeCommitmentPayloadStrict(wire);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
if (err instanceof CannotVerify)
|
|
408
|
+
return 'cannot_verify';
|
|
409
|
+
throw err; // a bug in the reader, not a property of the input — surface it
|
|
410
|
+
}
|
|
411
|
+
return typeof claimedHash === 'string' && claimedHash === commitmentHash(payload) ? 'match' : 'mismatch';
|
|
412
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -29,3 +29,32 @@ export declare const POLICY_DENIED = "POLICY_DENIED";
|
|
|
29
29
|
export declare const INVALID_SESSION_ID = "INVALID_SESSION_ID";
|
|
30
30
|
export declare const UNKNOWN_POLICY_VERSION = "UNKNOWN_POLICY_VERSION";
|
|
31
31
|
export declare const INVALID_POLICY_DEFINITION = "INVALID_POLICY_DEFINITION";
|
|
32
|
+
/** Register/UnregisterPolicy against a file-loaded (read-only) policy registry — FAILED_PRECONDITION. */
|
|
33
|
+
export declare const REGISTRY_READ_ONLY = "REGISTRY_READ_ONLY";
|
|
34
|
+
/** Malformed control-plane request (bad page_size/page_token, missing descriptor, …) — INVALID_ARGUMENT. */
|
|
35
|
+
export declare const INVALID_REQUEST = "INVALID_REQUEST";
|
|
36
|
+
/** Stream/watch subscriber fell behind — RESOURCE_EXHAUSTED; resubscribe with `afterSequence`. */
|
|
37
|
+
export declare const STREAM_LAGGED = "STREAM_LAGGED";
|
|
38
|
+
/** GetManifest for an agent this runtime does not host — NOT_FOUND. */
|
|
39
|
+
export declare const AGENT_NOT_FOUND = "AGENT_NOT_FOUND";
|
|
40
|
+
export declare const DENY_VETO = "veto";
|
|
41
|
+
export declare const DENY_VETO_HOLD = "veto_hold";
|
|
42
|
+
export declare const DENY_QUORUM_NOT_MET = "quorum_not_met";
|
|
43
|
+
export declare const DENY_THRESHOLD_NOT_MET = "threshold_not_met";
|
|
44
|
+
export declare const DENY_CONFIDENCE_BELOW_MINIMUM = "confidence_below_minimum";
|
|
45
|
+
export declare const DENY_EVALUATION_REQUIRED = "evaluation_required";
|
|
46
|
+
export declare const DENY_NO_VOTES = "no_votes";
|
|
47
|
+
export declare const DENY_DECLINE_OVER_APPROVAL = "decline_over_approval";
|
|
48
|
+
export declare const DENY_DECLINE_NOT_BACKED = "decline_not_backed";
|
|
49
|
+
export declare const DENY_CRITERION_NOT_MET = "criterion_not_met";
|
|
50
|
+
export declare const DENY_OUTPUT_REQUIRED = "output_required";
|
|
51
|
+
export declare const DENY_REASSIGNMENT_NOT_ALLOWED = "reassignment_not_allowed";
|
|
52
|
+
export declare const DENY_UNSUPPORTED_SCHEMA_VERSION = "unsupported_schema_version";
|
|
53
|
+
export declare const DENY_INVALID_RULES = "invalid_rules";
|
|
54
|
+
export declare const DENY_UNSPECIFIED = "unspecified";
|
|
55
|
+
/**
|
|
56
|
+
* The denial codes the reference evaluator emits. A custom evaluator may send
|
|
57
|
+
* others — `PolicyDenial.code` passes them through unchanged, so treat a code
|
|
58
|
+
* not in this set as {@link DENY_UNSPECIFIED}.
|
|
59
|
+
*/
|
|
60
|
+
export declare const KNOWN_DENY_CODES: ReadonlySet<string>;
|