macp-sdk-typescript 0.14.1 → 0.16.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 +18 -3
- 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/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.d.ts +20 -0
- package/dist/watchers.js +14 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,28 @@ 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.
|
|
7
|
+
## [0.16.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.15.0...v0.16.0) (2026-10-18)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **watchers:** SignalWatcher.events() surfaces sender_authenticated (runtime 0.9.0) ([#172](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/172)) ([57491cd](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/57491cd8e156c9f1650a918e36eea5ff9d2bdab1))
|
|
13
|
+
|
|
14
|
+
## [0.15.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.14.1...v0.15.0) (2026-10-17)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **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))
|
|
20
|
+
* **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)
|
|
21
|
+
* **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))
|
|
8
22
|
|
|
9
23
|
|
|
10
24
|
### Bug Fixes
|
|
11
25
|
|
|
12
|
-
* **
|
|
26
|
+
* **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))
|
|
13
27
|
|
|
14
|
-
## [
|
|
28
|
+
## [0.14.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.14.0...v0.14.1) (2026-10-03)
|
|
15
29
|
|
|
16
30
|
### Added
|
|
17
31
|
|
|
@@ -72,6 +86,7 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
72
86
|
covers a cleanly-ended watch stream alongside every other SDK failure.
|
|
73
87
|
Message strings are unchanged byte-for-byte; `error.code` remains
|
|
74
88
|
`undefined` on all 5 (locally raised, no gRPC status to attach).
|
|
89
|
+
- **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))
|
|
75
90
|
|
|
76
91
|
## [0.14.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.13.0...v0.14.0) (2026-10-01)
|
|
77
92
|
|
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>;
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.INVALID_POLICY_DEFINITION = exports.UNKNOWN_POLICY_VERSION = exports.INVALID_SESSION_ID = exports.POLICY_DENIED = exports.INTERNAL_ERROR = exports.RATE_LIMITED = exports.PAYLOAD_TOO_LARGE = exports.DUPLICATE_MESSAGE = exports.UNAUTHENTICATED = exports.FORBIDDEN = exports.MODE_NOT_SUPPORTED = exports.SESSION_NOT_OPEN = exports.SESSION_NOT_FOUND = exports.SESSION_ALREADY_EXISTS = exports.INVALID_ENVELOPE = exports.UNSUPPORTED_PROTOCOL_VERSION = exports.STANDARD_MODES = exports.MODE_MULTI_ROUND = exports.MODE_QUORUM = exports.MODE_HANDOFF = exports.MODE_TASK = exports.MODE_PROPOSAL = exports.MODE_DECISION = exports.DEFAULT_POLICY_VERSION = exports.DEFAULT_CONFIGURATION_VERSION = exports.DEFAULT_MODE_VERSION = exports.MACP_VERSION = void 0;
|
|
3
|
+
exports.KNOWN_DENY_CODES = exports.DENY_UNSPECIFIED = exports.DENY_INVALID_RULES = exports.DENY_UNSUPPORTED_SCHEMA_VERSION = exports.DENY_REASSIGNMENT_NOT_ALLOWED = exports.DENY_OUTPUT_REQUIRED = exports.DENY_CRITERION_NOT_MET = exports.DENY_DECLINE_NOT_BACKED = exports.DENY_DECLINE_OVER_APPROVAL = exports.DENY_NO_VOTES = exports.DENY_EVALUATION_REQUIRED = exports.DENY_CONFIDENCE_BELOW_MINIMUM = exports.DENY_THRESHOLD_NOT_MET = exports.DENY_QUORUM_NOT_MET = exports.DENY_VETO_HOLD = exports.DENY_VETO = exports.AGENT_NOT_FOUND = exports.STREAM_LAGGED = exports.INVALID_REQUEST = exports.REGISTRY_READ_ONLY = exports.INVALID_POLICY_DEFINITION = exports.UNKNOWN_POLICY_VERSION = exports.INVALID_SESSION_ID = exports.POLICY_DENIED = exports.INTERNAL_ERROR = exports.RATE_LIMITED = exports.PAYLOAD_TOO_LARGE = exports.DUPLICATE_MESSAGE = exports.UNAUTHENTICATED = exports.FORBIDDEN = exports.MODE_NOT_SUPPORTED = exports.SESSION_NOT_OPEN = exports.SESSION_NOT_FOUND = exports.SESSION_ALREADY_EXISTS = exports.INVALID_ENVELOPE = exports.UNSUPPORTED_PROTOCOL_VERSION = exports.STANDARD_MODES = exports.MODE_MULTI_ROUND = exports.MODE_QUORUM = exports.MODE_HANDOFF = exports.MODE_TASK = exports.MODE_PROPOSAL = exports.MODE_DECISION = exports.DEFAULT_POLICY_VERSION = exports.DEFAULT_CONFIGURATION_VERSION = exports.DEFAULT_MODE_VERSION = exports.MACP_VERSION = void 0;
|
|
4
4
|
exports.MACP_VERSION = '1.0';
|
|
5
5
|
exports.DEFAULT_MODE_VERSION = '1.0.0';
|
|
6
6
|
exports.DEFAULT_CONFIGURATION_VERSION = 'config.default';
|
|
@@ -34,3 +34,52 @@ exports.POLICY_DENIED = 'POLICY_DENIED';
|
|
|
34
34
|
exports.INVALID_SESSION_ID = 'INVALID_SESSION_ID';
|
|
35
35
|
exports.UNKNOWN_POLICY_VERSION = 'UNKNOWN_POLICY_VERSION';
|
|
36
36
|
exports.INVALID_POLICY_DEFINITION = 'INVALID_POLICY_DEFINITION';
|
|
37
|
+
// Control-plane codes added in runtime 0.9.0 (carried in `macp-error-code`
|
|
38
|
+
// metadata; see `MacpTransportError.macpErrorCode` / `MacpAckError.macpErrorCode`).
|
|
39
|
+
/** Register/UnregisterPolicy against a file-loaded (read-only) policy registry — FAILED_PRECONDITION. */
|
|
40
|
+
exports.REGISTRY_READ_ONLY = 'REGISTRY_READ_ONLY';
|
|
41
|
+
/** Malformed control-plane request (bad page_size/page_token, missing descriptor, …) — INVALID_ARGUMENT. */
|
|
42
|
+
exports.INVALID_REQUEST = 'INVALID_REQUEST';
|
|
43
|
+
/** Stream/watch subscriber fell behind — RESOURCE_EXHAUSTED; resubscribe with `afterSequence`. */
|
|
44
|
+
exports.STREAM_LAGGED = 'STREAM_LAGGED';
|
|
45
|
+
/** GetManifest for an agent this runtime does not host — NOT_FOUND. */
|
|
46
|
+
exports.AGENT_NOT_FOUND = 'AGENT_NOT_FOUND';
|
|
47
|
+
// Structured POLICY_DENIED denial codes (`PolicyDenial.code`, runtime ≥ 0.9.0;
|
|
48
|
+
// RFC-MACP-0012 §6.2). Open set — names match python-sdk's exports.
|
|
49
|
+
exports.DENY_VETO = 'veto';
|
|
50
|
+
exports.DENY_VETO_HOLD = 'veto_hold';
|
|
51
|
+
exports.DENY_QUORUM_NOT_MET = 'quorum_not_met';
|
|
52
|
+
exports.DENY_THRESHOLD_NOT_MET = 'threshold_not_met';
|
|
53
|
+
exports.DENY_CONFIDENCE_BELOW_MINIMUM = 'confidence_below_minimum';
|
|
54
|
+
exports.DENY_EVALUATION_REQUIRED = 'evaluation_required';
|
|
55
|
+
exports.DENY_NO_VOTES = 'no_votes';
|
|
56
|
+
exports.DENY_DECLINE_OVER_APPROVAL = 'decline_over_approval';
|
|
57
|
+
exports.DENY_DECLINE_NOT_BACKED = 'decline_not_backed';
|
|
58
|
+
exports.DENY_CRITERION_NOT_MET = 'criterion_not_met';
|
|
59
|
+
exports.DENY_OUTPUT_REQUIRED = 'output_required';
|
|
60
|
+
exports.DENY_REASSIGNMENT_NOT_ALLOWED = 'reassignment_not_allowed';
|
|
61
|
+
exports.DENY_UNSUPPORTED_SCHEMA_VERSION = 'unsupported_schema_version';
|
|
62
|
+
exports.DENY_INVALID_RULES = 'invalid_rules';
|
|
63
|
+
exports.DENY_UNSPECIFIED = 'unspecified';
|
|
64
|
+
/**
|
|
65
|
+
* The denial codes the reference evaluator emits. A custom evaluator may send
|
|
66
|
+
* others — `PolicyDenial.code` passes them through unchanged, so treat a code
|
|
67
|
+
* not in this set as {@link DENY_UNSPECIFIED}.
|
|
68
|
+
*/
|
|
69
|
+
exports.KNOWN_DENY_CODES = new Set([
|
|
70
|
+
exports.DENY_VETO,
|
|
71
|
+
exports.DENY_VETO_HOLD,
|
|
72
|
+
exports.DENY_QUORUM_NOT_MET,
|
|
73
|
+
exports.DENY_THRESHOLD_NOT_MET,
|
|
74
|
+
exports.DENY_CONFIDENCE_BELOW_MINIMUM,
|
|
75
|
+
exports.DENY_EVALUATION_REQUIRED,
|
|
76
|
+
exports.DENY_NO_VOTES,
|
|
77
|
+
exports.DENY_DECLINE_OVER_APPROVAL,
|
|
78
|
+
exports.DENY_DECLINE_NOT_BACKED,
|
|
79
|
+
exports.DENY_CRITERION_NOT_MET,
|
|
80
|
+
exports.DENY_OUTPUT_REQUIRED,
|
|
81
|
+
exports.DENY_REASSIGNMENT_NOT_ALLOWED,
|
|
82
|
+
exports.DENY_UNSUPPORTED_SCHEMA_VERSION,
|
|
83
|
+
exports.DENY_INVALID_RULES,
|
|
84
|
+
exports.DENY_UNSPECIFIED,
|
|
85
|
+
]);
|
package/dist/decision.js
CHANGED
|
@@ -95,7 +95,10 @@ class DecisionSession {
|
|
|
95
95
|
}
|
|
96
96
|
async raiseObjection(input) {
|
|
97
97
|
(0, validation_1.validateRequiredField)('proposalId', input.proposalId);
|
|
98
|
-
|
|
98
|
+
// Always send an explicit lowercase severity: runtime ≥ 0.9.0 sessions
|
|
99
|
+
// (semantics_rev 4) reject an empty one instead of defaulting to
|
|
100
|
+
// "medium", so the SDK applies that default itself (python-sdk parity).
|
|
101
|
+
const severity = (0, validation_1.validateSeverity)(input.severity ?? 'medium');
|
|
99
102
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
100
103
|
mode: constants_1.MODE_DECISION,
|
|
101
104
|
messageType: 'Objection',
|
package/dist/errors.d.ts
CHANGED
|
@@ -8,12 +8,62 @@ export declare class MacpTransportError extends MacpSdkError {
|
|
|
8
8
|
* `UNAUTHENTICATED`) when the underlying transport failure carried one.
|
|
9
9
|
* Lets consumers distinguish, for example, watch-stream consumer lag
|
|
10
10
|
* (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
|
|
11
|
-
* → do not reconnect)
|
|
12
|
-
*
|
|
11
|
+
* → do not reconnect). Absent for locally-raised transport errors. For a
|
|
12
|
+
* runtime-raised MACP error prefer {@link macpErrorCode}.
|
|
13
13
|
*/
|
|
14
14
|
readonly code?: string;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* MACP error-codes registry code (e.g. `SESSION_NOT_FOUND`, `POLICY_DENIED`)
|
|
17
|
+
* from the status's `macp-error-code` trailing metadata (runtime ≥ 0.9.0).
|
|
18
|
+
* Branch on this rather than {@link code}: since 0.9.0 the gRPC status
|
|
19
|
+
* follows the registry's gRPC column, and several registry codes share one
|
|
20
|
+
* status (e.g. `INVALID_ENVELOPE` and `INVALID_SESSION_ID` are both
|
|
21
|
+
* `INVALID_ARGUMENT`). Absent for older runtimes, for statuses the runtime
|
|
22
|
+
* raised without a registry code, and for locally-raised errors.
|
|
23
|
+
*/
|
|
24
|
+
readonly macpErrorCode?: string;
|
|
25
|
+
constructor(message: string, code?: string, macpErrorCode?: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One structured `POLICY_DENIED` reason (RFC-MACP-0012 §6.2, runtime ≥ 0.9.0).
|
|
29
|
+
*
|
|
30
|
+
* `code` is an open set: the reference evaluator emits `veto`, `veto_hold`,
|
|
31
|
+
* `quorum_not_met`, `threshold_not_met`, `confidence_below_minimum`,
|
|
32
|
+
* `evaluation_required`, `no_votes`, `decline_over_approval`,
|
|
33
|
+
* `decline_not_backed`, `criterion_not_met`, `output_required`,
|
|
34
|
+
* `reassignment_not_allowed`, `unsupported_schema_version`, `invalid_rules`
|
|
35
|
+
* and `unspecified` (exported as the `DENY_*` constants and
|
|
36
|
+
* {@link KNOWN_DENY_CODES}), but a custom evaluator may mint its own. `code`
|
|
37
|
+
* is the wire value passed through unchanged — treat one not in
|
|
38
|
+
* `KNOWN_DENY_CODES` as `unspecified`. `participants` lists the participant
|
|
39
|
+
* ids the reason concerns (e.g. a veto's objectors); since 0.9.0 they are no
|
|
40
|
+
* longer embedded in `message`.
|
|
41
|
+
*/
|
|
42
|
+
export interface PolicyDenial {
|
|
43
|
+
code: string;
|
|
44
|
+
message: string;
|
|
45
|
+
participants: string[];
|
|
16
46
|
}
|
|
47
|
+
/** Parsed MACP error `details` JSON: legacy `reasons` strings plus structured `denials`. */
|
|
48
|
+
export interface MacpErrorDetails {
|
|
49
|
+
reasons: string[];
|
|
50
|
+
denials: PolicyDenial[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse the JSON `details` the runtime attaches to a `POLICY_DENIED` error —
|
|
54
|
+
* found on `Ack.error.details`, an inline stream `MACPError.details`, or the
|
|
55
|
+
* `macp-error-details-bin` trailing metadata. Never throws: missing,
|
|
56
|
+
* non-JSON, or malformed input yields empty arrays; a `denials` entry that is
|
|
57
|
+
* not an object is dropped, and one without a string `message` is kept with
|
|
58
|
+
* `message: ''`. Pre-0.9.0 runtimes send `reasons` only,
|
|
59
|
+
* so `denials` is `[]` for them.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseErrorDetails(details: Buffer | Uint8Array | string | null | undefined): MacpErrorDetails;
|
|
62
|
+
/** Value of the `macp-error-code` trailing-metadata key, if present (runtime ≥ 0.9.0). */
|
|
63
|
+
export declare function macpErrorCodeFromMetadata(grpcMetadata?: Array<{
|
|
64
|
+
key: string;
|
|
65
|
+
value: string | Buffer;
|
|
66
|
+
}>): string | undefined;
|
|
17
67
|
/**
|
|
18
68
|
* Structured NACK record. Mirrors python-sdk's `AckFailure` dataclass so
|
|
19
69
|
* cross-SDK code can share the same shape when logging or persisting a
|
|
@@ -25,6 +75,11 @@ export interface AckFailure {
|
|
|
25
75
|
sessionId: string;
|
|
26
76
|
messageId: string;
|
|
27
77
|
reasons: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Structured policy-denial reasons (runtime ≥ 0.9.0). Empty for non-policy
|
|
80
|
+
* NACKs and for older runtimes, which send `reasons` strings only.
|
|
81
|
+
*/
|
|
82
|
+
denials: PolicyDenial[];
|
|
28
83
|
}
|
|
29
84
|
export declare class MacpAckError extends MacpSdkError {
|
|
30
85
|
readonly ack: Ack;
|
|
@@ -35,13 +90,19 @@ export declare class MacpAckError extends MacpSdkError {
|
|
|
35
90
|
}>;
|
|
36
91
|
/** Structured NACK record — parity with python-sdk `MacpAckError.failure`. */
|
|
37
92
|
readonly failure: AckFailure;
|
|
93
|
+
/**
|
|
94
|
+
* The registry code from the `macp-error-code` metadata (runtime ≥ 0.9.0)
|
|
95
|
+
* when this NACK was synthesized from a gRPC status. Usually equal to
|
|
96
|
+
* `failure.code`; differs on the registry-mutation RPCs, which keep
|
|
97
|
+
* `failure.code: 'FAILED_PRECONDITION'` for compatibility while this names
|
|
98
|
+
* the precise cause (e.g. `REGISTRY_READ_ONLY`).
|
|
99
|
+
*/
|
|
100
|
+
readonly macpErrorCode?: string;
|
|
38
101
|
constructor(ack: Ack, grpcMetadata?: Array<{
|
|
39
102
|
key: string;
|
|
40
103
|
value: string | Buffer;
|
|
41
104
|
}>);
|
|
42
|
-
private static
|
|
43
|
-
private static _parseAckReasons;
|
|
44
|
-
private static _parseGrpcMetadataReasons;
|
|
105
|
+
private static _extractDetails;
|
|
45
106
|
}
|
|
46
107
|
export declare class MacpSessionError extends MacpSdkError {
|
|
47
108
|
constructor(message: string);
|
package/dist/errors.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MacpIdentityMismatchError = exports.MacpRetryError = exports.MacpTimeoutError = exports.MacpSessionError = exports.MacpAckError = exports.MacpTransportError = exports.MacpSdkError = void 0;
|
|
4
|
+
exports.parseErrorDetails = parseErrorDetails;
|
|
5
|
+
exports.macpErrorCodeFromMetadata = macpErrorCodeFromMetadata;
|
|
4
6
|
class MacpSdkError extends Error {
|
|
5
7
|
constructor(message) {
|
|
6
8
|
super(message);
|
|
@@ -14,80 +16,121 @@ class MacpTransportError extends MacpSdkError {
|
|
|
14
16
|
* `UNAUTHENTICATED`) when the underlying transport failure carried one.
|
|
15
17
|
* Lets consumers distinguish, for example, watch-stream consumer lag
|
|
16
18
|
* (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
|
|
17
|
-
* → do not reconnect)
|
|
18
|
-
*
|
|
19
|
+
* → do not reconnect). Absent for locally-raised transport errors. For a
|
|
20
|
+
* runtime-raised MACP error prefer {@link macpErrorCode}.
|
|
19
21
|
*/
|
|
20
22
|
code;
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* MACP error-codes registry code (e.g. `SESSION_NOT_FOUND`, `POLICY_DENIED`)
|
|
25
|
+
* from the status's `macp-error-code` trailing metadata (runtime ≥ 0.9.0).
|
|
26
|
+
* Branch on this rather than {@link code}: since 0.9.0 the gRPC status
|
|
27
|
+
* follows the registry's gRPC column, and several registry codes share one
|
|
28
|
+
* status (e.g. `INVALID_ENVELOPE` and `INVALID_SESSION_ID` are both
|
|
29
|
+
* `INVALID_ARGUMENT`). Absent for older runtimes, for statuses the runtime
|
|
30
|
+
* raised without a registry code, and for locally-raised errors.
|
|
31
|
+
*/
|
|
32
|
+
macpErrorCode;
|
|
33
|
+
constructor(message, code, macpErrorCode) {
|
|
22
34
|
super(message);
|
|
23
35
|
this.name = 'MacpTransportError';
|
|
24
36
|
this.code = code;
|
|
37
|
+
if (macpErrorCode !== undefined)
|
|
38
|
+
this.macpErrorCode = macpErrorCode;
|
|
25
39
|
}
|
|
26
40
|
}
|
|
27
41
|
exports.MacpTransportError = MacpTransportError;
|
|
42
|
+
/**
|
|
43
|
+
* Parse the JSON `details` the runtime attaches to a `POLICY_DENIED` error —
|
|
44
|
+
* found on `Ack.error.details`, an inline stream `MACPError.details`, or the
|
|
45
|
+
* `macp-error-details-bin` trailing metadata. Never throws: missing,
|
|
46
|
+
* non-JSON, or malformed input yields empty arrays; a `denials` entry that is
|
|
47
|
+
* not an object is dropped, and one without a string `message` is kept with
|
|
48
|
+
* `message: ''`. Pre-0.9.0 runtimes send `reasons` only,
|
|
49
|
+
* so `denials` is `[]` for them.
|
|
50
|
+
*/
|
|
51
|
+
function parseErrorDetails(details) {
|
|
52
|
+
const empty = { reasons: [], denials: [] };
|
|
53
|
+
if (details === null || details === undefined)
|
|
54
|
+
return empty;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
const raw = typeof details === 'string' ? details : Buffer.from(details).toString('utf-8');
|
|
58
|
+
if (raw.length === 0)
|
|
59
|
+
return empty;
|
|
60
|
+
parsed = JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return empty;
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
66
|
+
return empty;
|
|
67
|
+
const obj = parsed;
|
|
68
|
+
const reasons = Array.isArray(obj.reasons) ? obj.reasons.filter((r) => typeof r === 'string') : [];
|
|
69
|
+
const denials = [];
|
|
70
|
+
if (Array.isArray(obj.denials)) {
|
|
71
|
+
for (const d of obj.denials) {
|
|
72
|
+
if (typeof d !== 'object' || d === null)
|
|
73
|
+
continue;
|
|
74
|
+
const { code, message, participants } = d;
|
|
75
|
+
denials.push({
|
|
76
|
+
code: typeof code === 'string' && code.length > 0 ? code : 'unspecified',
|
|
77
|
+
// A denial without a string message is kept with '' (python-sdk
|
|
78
|
+
// parity, issue #168) — its code and participants still matter.
|
|
79
|
+
message: typeof message === 'string' ? message : '',
|
|
80
|
+
participants: Array.isArray(participants) ? participants.filter((p) => typeof p === 'string') : [],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { reasons, denials };
|
|
85
|
+
}
|
|
86
|
+
/** Value of the `macp-error-code` trailing-metadata key, if present (runtime ≥ 0.9.0). */
|
|
87
|
+
function macpErrorCodeFromMetadata(grpcMetadata) {
|
|
88
|
+
const entry = grpcMetadata?.find((item) => item.key === 'macp-error-code');
|
|
89
|
+
if (!entry)
|
|
90
|
+
return undefined;
|
|
91
|
+
const value = typeof entry.value === 'string' ? entry.value : entry.value.toString('utf-8');
|
|
92
|
+
return value.length > 0 ? value : undefined;
|
|
93
|
+
}
|
|
28
94
|
class MacpAckError extends MacpSdkError {
|
|
29
95
|
ack;
|
|
30
96
|
/** Optional gRPC trailing metadata for extracting additional error details. */
|
|
31
97
|
grpcMetadata;
|
|
32
98
|
/** Structured NACK record — parity with python-sdk `MacpAckError.failure`. */
|
|
33
99
|
failure;
|
|
100
|
+
/**
|
|
101
|
+
* The registry code from the `macp-error-code` metadata (runtime ≥ 0.9.0)
|
|
102
|
+
* when this NACK was synthesized from a gRPC status. Usually equal to
|
|
103
|
+
* `failure.code`; differs on the registry-mutation RPCs, which keep
|
|
104
|
+
* `failure.code: 'FAILED_PRECONDITION'` for compatibility while this names
|
|
105
|
+
* the precise cause (e.g. `REGISTRY_READ_ONLY`).
|
|
106
|
+
*/
|
|
107
|
+
macpErrorCode;
|
|
34
108
|
constructor(ack, grpcMetadata) {
|
|
35
109
|
super(`${ack.error?.code ?? 'UNKNOWN'}: ${ack.error?.message ?? 'runtime returned nack'}`);
|
|
36
110
|
this.name = 'MacpAckError';
|
|
37
111
|
this.ack = ack;
|
|
38
112
|
this.grpcMetadata = grpcMetadata;
|
|
39
|
-
const reasons = MacpAckError.
|
|
113
|
+
const { reasons, denials } = MacpAckError._extractDetails(ack, grpcMetadata);
|
|
40
114
|
this.failure = {
|
|
41
115
|
code: ack.error?.code ?? 'UNKNOWN',
|
|
42
116
|
message: ack.error?.message ?? 'runtime returned nack',
|
|
43
117
|
sessionId: ack.sessionId ?? ack.error?.sessionId ?? '',
|
|
44
118
|
messageId: ack.messageId ?? ack.error?.messageId ?? '',
|
|
45
119
|
reasons,
|
|
120
|
+
denials,
|
|
46
121
|
};
|
|
122
|
+
const macpErrorCode = macpErrorCodeFromMetadata(grpcMetadata);
|
|
123
|
+
if (macpErrorCode !== undefined)
|
|
124
|
+
this.macpErrorCode = macpErrorCode;
|
|
47
125
|
}
|
|
48
|
-
static
|
|
49
|
-
// First try:
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
53
|
-
// Second try:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
static _parseAckReasons(ack) {
|
|
57
|
-
if (!ack.error?.details)
|
|
58
|
-
return [];
|
|
59
|
-
try {
|
|
60
|
-
const details = ack.error.details;
|
|
61
|
-
const raw = Buffer.isBuffer(details) ? details.toString('utf-8') : String(details);
|
|
62
|
-
const parsed = JSON.parse(raw);
|
|
63
|
-
const reasons = parsed.reasons;
|
|
64
|
-
return Array.isArray(reasons) ? reasons : [];
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
static _parseGrpcMetadataReasons(grpcMetadata) {
|
|
71
|
-
if (!grpcMetadata)
|
|
72
|
-
return [];
|
|
73
|
-
try {
|
|
74
|
-
for (const item of grpcMetadata) {
|
|
75
|
-
if (item.key === 'macp-error-details-bin') {
|
|
76
|
-
const data = Buffer.isBuffer(item.value)
|
|
77
|
-
? item.value.toString('utf-8')
|
|
78
|
-
: typeof item.value === 'string'
|
|
79
|
-
? item.value
|
|
80
|
-
: Buffer.from(item.value).toString('utf-8');
|
|
81
|
-
const parsed = JSON.parse(data);
|
|
82
|
-
const reasons = parsed.reasons;
|
|
83
|
-
return Array.isArray(reasons) ? reasons : [];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// Intentionally swallowed — matches Python's bare except
|
|
89
|
-
}
|
|
90
|
-
return [];
|
|
126
|
+
static _extractDetails(ack, grpcMetadata) {
|
|
127
|
+
// First try: ack.error.details (matches Python _parse_ack_reasons)
|
|
128
|
+
const fromAck = parseErrorDetails(ack.error?.details);
|
|
129
|
+
if (fromAck.reasons.length > 0 || fromAck.denials.length > 0)
|
|
130
|
+
return fromAck;
|
|
131
|
+
// Second try: gRPC trailing metadata (matches Python _parse_grpc_metadata_reasons)
|
|
132
|
+
const entry = grpcMetadata?.find((item) => item.key === 'macp-error-details-bin');
|
|
133
|
+
return parseErrorDetails(entry?.value);
|
|
91
134
|
}
|
|
92
135
|
}
|
|
93
136
|
exports.MacpAckError = MacpAckError;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Auth, type AuthConfig, type BearerAuthOptions } from './auth';
|
|
2
2
|
export * from './base-session';
|
|
3
3
|
export * from './client';
|
|
4
|
-
export { commitmentHash, isCanonicalCommitmentHash } from './commitment-hash';
|
|
4
|
+
export { commitmentHash, isCanonicalCommitmentHash, verifyCommitmentHash, type VerifyOutcome } from './commitment-hash';
|
|
5
5
|
export * from './constants';
|
|
6
6
|
export * from './decision';
|
|
7
7
|
export * from './envelope';
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.agent = exports.VERSION = exports.logger = exports.configureLogging = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
|
|
39
|
+
exports.agent = exports.VERSION = exports.logger = exports.configureLogging = exports.verifyCommitmentHash = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
|
|
40
40
|
// Auth: export only the public constructors and types. The helpers
|
|
41
41
|
// `validateAuth`, `authSender`, `assertSenderMatchesIdentity`, and
|
|
42
42
|
// `metadataFromAuth` are SDK-internal — importable from the `./auth`
|
|
@@ -46,7 +46,8 @@ var auth_1 = require("./auth");
|
|
|
46
46
|
Object.defineProperty(exports, "Auth", { enumerable: true, get: function () { return auth_1.Auth; } });
|
|
47
47
|
__exportStar(require("./base-session"), exports);
|
|
48
48
|
__exportStar(require("./client"), exports);
|
|
49
|
-
// Commitment hash: export `commitmentHash` and
|
|
49
|
+
// Commitment hash: export `commitmentHash`, `isCanonicalCommitmentHash`, and
|
|
50
|
+
// the RFC-MACP-0013 §6.1 verifier `verifyCommitmentHash`.
|
|
50
51
|
// `canonicalizeCommitmentPayload` is the projection/JCS half of the
|
|
51
52
|
// algorithm — importable from the `./commitment-hash` submodule (as the
|
|
52
53
|
// vector-runner tests do) but kept off the top-level surface to match
|
|
@@ -55,6 +56,7 @@ __exportStar(require("./client"), exports);
|
|
|
55
56
|
var commitment_hash_1 = require("./commitment-hash");
|
|
56
57
|
Object.defineProperty(exports, "commitmentHash", { enumerable: true, get: function () { return commitment_hash_1.commitmentHash; } });
|
|
57
58
|
Object.defineProperty(exports, "isCanonicalCommitmentHash", { enumerable: true, get: function () { return commitment_hash_1.isCanonicalCommitmentHash; } });
|
|
59
|
+
Object.defineProperty(exports, "verifyCommitmentHash", { enumerable: true, get: function () { return commitment_hash_1.verifyCommitmentHash; } });
|
|
58
60
|
__exportStar(require("./constants"), exports);
|
|
59
61
|
__exportStar(require("./decision"), exports);
|
|
60
62
|
__exportStar(require("./envelope"), exports);
|
package/dist/types.d.ts
CHANGED
|
@@ -173,6 +173,7 @@ export interface DecisionEvaluationPayload {
|
|
|
173
173
|
export interface DecisionObjectionPayload {
|
|
174
174
|
proposalId: string;
|
|
175
175
|
reason: string;
|
|
176
|
+
/** `critical` | `high` | `medium` | `low` (case-insensitive; sent lowercase). Defaults to `medium`. */
|
|
176
177
|
severity?: string;
|
|
177
178
|
}
|
|
178
179
|
export interface DecisionVotePayload {
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Validate that `sid` is a lowercase UUID v4/v7 or base64url (22
|
|
2
|
+
* Validate that `sid` is a lowercase UUID v4/v7 or base64url (22–128 chars).
|
|
3
3
|
*
|
|
4
4
|
* Mirrors the runtime's **no-fall-through** rule (issue #108.1, and
|
|
5
5
|
* `macp-sdk-python`'s `validate_session_id`, `validation.py:33-58`, which
|
package/dist/validation.js
CHANGED
|
@@ -20,9 +20,12 @@ const errors_1 = require("./errors");
|
|
|
20
20
|
const UUID_SHAPE_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
21
21
|
// Strict lowercase UUID v4/v7 (version nibble 4 or 7, RFC 9562 variant 8/9/a/b).
|
|
22
22
|
const UUID_V4V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
23
|
-
|
|
23
|
+
// 22–128 chars: the 128 cap is RFC-MACP-0004 §11.2's portability floor, which
|
|
24
|
+
// runtime ≥ 0.9.0 enforces at SessionStart (INVALID_SESSION_ID). The charset is
|
|
25
|
+
// ASCII, so 128 chars == 128 bytes.
|
|
26
|
+
const BASE64URL_RE = /^[A-Za-z0-9_-]{22,128}$/;
|
|
24
27
|
/**
|
|
25
|
-
* Validate that `sid` is a lowercase UUID v4/v7 or base64url (22
|
|
28
|
+
* Validate that `sid` is a lowercase UUID v4/v7 or base64url (22–128 chars).
|
|
26
29
|
*
|
|
27
30
|
* Mirrors the runtime's **no-fall-through** rule (issue #108.1, and
|
|
28
31
|
* `macp-sdk-python`'s `validate_session_id`, `validation.py:33-58`, which
|
|
@@ -44,7 +47,7 @@ function validateSessionId(sid) {
|
|
|
44
47
|
}
|
|
45
48
|
if (BASE64URL_RE.test(sid))
|
|
46
49
|
return;
|
|
47
|
-
throw new errors_1.MacpSessionError(`session_id must be a lowercase UUID v4/v7 or base64url (22
|
|
50
|
+
throw new errors_1.MacpSessionError(`session_id must be a lowercase UUID v4/v7 or base64url (22–128 chars), got: ${sid}`);
|
|
48
51
|
}
|
|
49
52
|
const VALID_VOTES = new Set(['APPROVE', 'REJECT', 'ABSTAIN']);
|
|
50
53
|
function validateVote(value) {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/watchers.d.ts
CHANGED
|
@@ -53,13 +53,33 @@ export declare class RootsWatcher {
|
|
|
53
53
|
* A missing/invalid credential surfaces as `code === 'UNAUTHENTICATED'` — fix
|
|
54
54
|
* auth, do not reconnect.
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* One `WatchSignals` frame: the Signal envelope plus whether the runtime
|
|
58
|
+
* authenticated its `sender` (RFC-MACP-0004 §4.1, runtime ≥ 0.9.0).
|
|
59
|
+
*/
|
|
60
|
+
export interface SignalEvent {
|
|
61
|
+
envelope: Envelope;
|
|
62
|
+
/**
|
|
63
|
+
* `true` only when the runtime authenticated the Signal's `sender` (JWT or
|
|
64
|
+
* static-token auth). `false` in dev mode and on runtimes that predate the
|
|
65
|
+
* field — the safe direction. A Signal with `false` MUST NOT influence
|
|
66
|
+
* operational decisions (RFC-MACP-0004 §4.1).
|
|
67
|
+
*/
|
|
68
|
+
senderAuthenticated: boolean;
|
|
69
|
+
}
|
|
56
70
|
export declare class SignalWatcher {
|
|
57
71
|
private readonly client;
|
|
58
72
|
private readonly auth?;
|
|
59
73
|
constructor(client: MacpClient, options?: {
|
|
60
74
|
auth?: AuthConfig;
|
|
61
75
|
});
|
|
76
|
+
/**
|
|
77
|
+
* Signal envelopes only. Does not say whether a Signal's `sender` was
|
|
78
|
+
* authenticated — use {@link events} wherever a Signal can drive a decision.
|
|
79
|
+
*/
|
|
62
80
|
signals(signal?: AbortSignal): AsyncGenerator<Envelope, void, void>;
|
|
81
|
+
/** Signal envelopes paired with the runtime's `sender_authenticated` verdict. */
|
|
82
|
+
events(signal?: AbortSignal): AsyncGenerator<SignalEvent, void, void>;
|
|
63
83
|
watch(handler: (envelope: Envelope) => void | Promise<void>): Promise<void>;
|
|
64
84
|
nextSignal(): Promise<Envelope>;
|
|
65
85
|
}
|
package/dist/watchers.js
CHANGED
|
@@ -55,7 +55,9 @@ function serverStreamToAsyncGenerator(stream) {
|
|
|
55
55
|
// WatchSignals with UNAUTHENTICATED (fix auth, do NOT reconnect).
|
|
56
56
|
const code = (0, client_1.grpcStatusName)(error.code);
|
|
57
57
|
const serviceErr = error;
|
|
58
|
-
const wrapped = new errors_1.MacpTransportError(serviceErr.details || error.message, code)
|
|
58
|
+
const wrapped = new errors_1.MacpTransportError(serviceErr.details || error.message, code, (0, errors_1.macpErrorCodeFromMetadata)(serviceErr.metadata
|
|
59
|
+
? serviceErr.metadata.get('macp-error-code').map((value) => ({ key: 'macp-error-code', value }))
|
|
60
|
+
: undefined));
|
|
59
61
|
queue.push({ error: wrapped });
|
|
60
62
|
if (resolve) {
|
|
61
63
|
resolve();
|
|
@@ -164,16 +166,6 @@ class RootsWatcher {
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
exports.RootsWatcher = RootsWatcher;
|
|
167
|
-
/**
|
|
168
|
-
* Watches the ambient signal plane. **Requires auth** as of runtime 0.5.0: if
|
|
169
|
-
* no credential is configured on the watcher or the client, iterating
|
|
170
|
-
* {@link SignalWatcher.signals} throws immediately (via `client.watchSignals`).
|
|
171
|
-
*
|
|
172
|
-
* Consumer lag terminates the stream with a coded `MacpTransportError`
|
|
173
|
-
* (`code === 'RESOURCE_EXHAUSTED'`); the correct response is to reconnect.
|
|
174
|
-
* A missing/invalid credential surfaces as `code === 'UNAUTHENTICATED'` — fix
|
|
175
|
-
* auth, do not reconnect.
|
|
176
|
-
*/
|
|
177
169
|
class SignalWatcher {
|
|
178
170
|
client;
|
|
179
171
|
auth;
|
|
@@ -181,15 +173,25 @@ class SignalWatcher {
|
|
|
181
173
|
this.client = client;
|
|
182
174
|
this.auth = options?.auth;
|
|
183
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Signal envelopes only. Does not say whether a Signal's `sender` was
|
|
178
|
+
* authenticated — use {@link events} wherever a Signal can drive a decision.
|
|
179
|
+
*/
|
|
184
180
|
async *signals(signal) {
|
|
181
|
+
for await (const event of this.events(signal))
|
|
182
|
+
yield event.envelope;
|
|
183
|
+
}
|
|
184
|
+
/** Signal envelopes paired with the runtime's `sender_authenticated` verdict. */
|
|
185
|
+
async *events(signal) {
|
|
185
186
|
const stream = this.client.watchSignals(this.auth);
|
|
186
187
|
if (signal) {
|
|
187
188
|
signal.addEventListener('abort', () => stream.cancel(), { once: true });
|
|
188
189
|
}
|
|
189
190
|
const gen = serverStreamToAsyncGenerator(stream);
|
|
190
191
|
for await (const response of gen) {
|
|
192
|
+
// Strict `=== true`: anything but an explicit true is unauthenticated.
|
|
191
193
|
if (response.envelope)
|
|
192
|
-
yield response.envelope;
|
|
194
|
+
yield { envelope: response.envelope, senderAuthenticated: response.senderAuthenticated === true };
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
async watch(handler) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "TypeScript SDK for the MACP Rust runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@grpc/grpc-js": "^1.14.3",
|
|
58
58
|
"@grpc/proto-loader": "^0.8.0",
|
|
59
|
-
"@multiagentcoordinationprotocol/proto": "^0.1.
|
|
59
|
+
"@multiagentcoordinationprotocol/proto": "^0.1.11",
|
|
60
60
|
"protobufjs": "^8.6.5"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|