macp-sdk-typescript 0.19.0 → 0.21.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 +14 -0
- package/README.md +32 -1
- package/dist/client.d.ts +55 -1
- package/dist/client.js +120 -17
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/otel.d.ts +54 -0
- package/dist/otel.js +179 -0
- package/dist/receipt.d.ts +42 -0
- package/dist/receipt.js +52 -0
- package/dist/types.d.ts +17 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.21.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.20.0...v0.21.0) (2026-10-26)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* optional OpenTelemetry send spans and W3C trace propagation (W5 S2) ([#212](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/212)) ([28ce7a1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/28ce7a149e6803f5828416eafbc73e4067f80cd7))
|
|
13
|
+
|
|
14
|
+
## [0.20.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.19.0...v0.20.0) (2026-10-26)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* runtime-issued decision receipts — getDecisionReceipt, getReceiptKeys, trustedKeysFromJwks (RFC-MACP-0013 §13) ([#210](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/210)) ([931d9e1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/931d9e13d7695a9f4659d0538e85dcb709eaeb20))
|
|
20
|
+
|
|
7
21
|
## [0.19.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.18.0...v0.19.0) (2026-10-26)
|
|
8
22
|
|
|
9
23
|
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ client.close();
|
|
|
72
72
|
|
|
73
73
|
The SDK uses a **three-layer** design:
|
|
74
74
|
|
|
75
|
-
- **Low-level transport** (`MacpClient`): typed wrappers around every `MACPRuntimeService` RPC — `initialize`, `send`, `openStream`, `getSession`, `cancelSession`, `suspendSession`, `resumeSession`, `listSessions`, `getManifest`, `listModes`, `listRoots`, `listExtModes`, `registerExtMode`, `unregisterExtMode`, `promoteMode`, the policy RPCs (`register`/`unregister`/`get`/`listPolicies`, and `whatIfReplay` where the runtime advertises it), plus streaming watchers (`ModeRegistryWatcher`, `RootsWatcher`, `SignalWatcher`, `PolicyWatcher`, `SessionLifecycleWatcher`).
|
|
75
|
+
- **Low-level transport** (`MacpClient`): typed wrappers around every `MACPRuntimeService` RPC — `initialize`, `send`, `openStream`, `getSession`, `cancelSession`, `suspendSession`, `resumeSession`, `listSessions`, `getManifest`, `listModes`, `listRoots`, `listExtModes`, `registerExtMode`, `unregisterExtMode`, `promoteMode`, the policy RPCs (`register`/`unregister`/`get`/`listPolicies`, and `whatIfReplay` where the runtime advertises it), `getDecisionReceipt`/`getReceiptKeys` where the runtime advertises receipts, plus streaming watchers (`ModeRegistryWatcher`, `RootsWatcher`, `SignalWatcher`, `PolicyWatcher`, `SessionLifecycleWatcher`).
|
|
76
76
|
- **High-level session helpers**: one class per coordination mode (`DecisionSession`, `ProposalSession`, `TaskSession`, `HandoffSession`, `QuorumSession`) — wraps `MacpClient`, builds envelopes, encodes payloads, and maintains a local state projection.
|
|
77
77
|
- **Agent framework** (`src/agent/`): event-driven `Participant` with strategy-based handlers, bootstrap runner (`fromBootstrap`), gRPC/HTTP transports, and the optional cancel-callback HTTP server.
|
|
78
78
|
|
|
@@ -463,12 +463,43 @@ const result = verifyReceipt(receipt, { 'prod-2026': 'TDVzkxaSf6_Oxu7mmn5TbCkF80
|
|
|
463
463
|
| `attestations[i]` | `valid`, `invalid`, `unknown_key` or `unsupported` (any other `alg`, and every `vendor` attestation) |
|
|
464
464
|
| `verdict` | `attested` (integrity holds and at least one attestation is valid), `integrity_only`, `unverified` (integrity fails, so attestations are not checked), or `refused` (unknown `receipt_format`) |
|
|
465
465
|
|
|
466
|
+
To trust a published key set (a JWK Set, such as a runtime's `GetReceiptKeys` response; RFC-MACP-0013 §13), convert it with `trustedKeysFromJwks(jwks)`:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { trustedKeysFromJwks, verifyReceipt } from 'macp-sdk-typescript/receipt';
|
|
470
|
+
|
|
471
|
+
const trustedKeys = trustedKeysFromJwks(jwks); // { 'example-1': 'TDVzkxaSf6_…' }
|
|
472
|
+
verifyReceipt(receipt, trustedKeys).verdict; // 'attested'
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
It keeps only Ed25519 public keys (`kty` `OKP`, `crv` `Ed25519`) with a `kid` and an `x`, whose `alg` (if any) is `EdDSA` and `use` (if any) is `sig`. It drops a key that carries a private member (`d`, `k`, …) and any `kid` that more than one entry of the set claims, even when the other entry is a key it can't use (an EC key, say). It never throws: anything that isn't a key set gives `{}`. Deciding to trust the set is still up to you.
|
|
476
|
+
|
|
477
|
+
### Fetching a runtime-issued receipt
|
|
478
|
+
|
|
479
|
+
A runtime that advertises `capabilities.decisionReceipt.getDecisionReceipt` issues receipts itself (RFC-MACP-0013 §13). Fetch one for a resolved session, fetch the runtime's key set, and verify:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const { receipt, raw } = await client.getDecisionReceipt(sessionId); // raw: the exact bytes served
|
|
483
|
+
const jwks = await client.getReceiptKeys(); // { keys: [] } from an unsigned runtime
|
|
484
|
+
verifyReceipt(receipt, trustedKeysFromJwks(jwks)).verdict; // 'attested', or 'integrity_only' if unsigned
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Both methods throw `MacpSdkError` without sending anything when the runtime doesn't advertise the capability. A session the runtime can't issue a receipt for (not resolved yet, or resolved without a `CommitmentPayload`) gives `MacpTransportError` with `macpErrorCode` `RECEIPT_UNAVAILABLE`.
|
|
488
|
+
|
|
466
489
|
If you hold the snake_case §3 projection (a receipt's `commitment`) instead of a `CommitmentPayload`, convert it with `commitmentFromProjection(projection)`.
|
|
467
490
|
|
|
468
491
|
`verifyReceipt` never throws. `buildReceipt` throws `MacpSdkError` on a missing field, and when `commitmentWire` is not the wire encoding of the commitment. Signing is deterministic: the same inputs always produce the same receipt.
|
|
469
492
|
|
|
470
493
|
The `macp-sdk-typescript/receipt` subpath is browser-safe: it pulls in no gRPC, no `node:crypto` and no `Buffer`. The same functions are also exported from the package root. The tests replay the spec's `schemas/conformance/receipt/` vectors and rebuild the spec's example receipt byte for byte.
|
|
471
494
|
|
|
495
|
+
## OpenTelemetry
|
|
496
|
+
|
|
497
|
+
Install the optional peer dependency `@opentelemetry/api` and register a tracer provider. The SDK then:
|
|
498
|
+
- emits one CLIENT span per `send()`, named `send {macp.mode.id}`, with the MACP semantic-convention attributes;
|
|
499
|
+
- puts W3C `traceparent`/`tracestate` in the gRPC metadata of every call made while a valid span context is active.
|
|
500
|
+
|
|
501
|
+
It never records payloads, `reason` text or error messages. Without the package, nothing is loaded or emitted. See [docs/guides/observability.md](docs/guides/observability.md) for the attribute set, the outcome rules and the limitations.
|
|
502
|
+
|
|
472
503
|
## Error Handling
|
|
473
504
|
|
|
474
505
|
```typescript
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as grpc from '@grpc/grpc-js';
|
|
2
2
|
import { type AuthConfig } from './auth';
|
|
3
3
|
import { ProtoRegistry } from './proto-registry';
|
|
4
|
+
import type { DecisionReceiptResult, ReceiptKeySet } from './receipt';
|
|
4
5
|
import type { Ack, AgentManifest, Envelope, InitializeResult, ModeDescriptor, PolicyDescriptor, SessionMetadata, Root, WhatIfReplayResult } from './types';
|
|
5
6
|
interface MacpClientOptions {
|
|
6
7
|
address: string;
|
|
@@ -99,12 +100,26 @@ export declare class MacpClient {
|
|
|
99
100
|
private readonly defaultDeadlineMs?;
|
|
100
101
|
private readonly clientName;
|
|
101
102
|
private readonly clientVersion;
|
|
102
|
-
/** The last {@link initialize} result;
|
|
103
|
+
/** The last {@link initialize} result; capability-gated RPCs read its capabilities. */
|
|
103
104
|
private initializeResult?;
|
|
105
|
+
/** An `Initialize` that {@link requireCapability} started and is still in flight. */
|
|
106
|
+
private pendingInit?;
|
|
104
107
|
/** The runtime's capabilities from the last {@link initialize}; `undefined` before one. */
|
|
105
108
|
get serverCapabilities(): Record<string, unknown> | undefined;
|
|
106
109
|
constructor(options: MacpClientOptions);
|
|
107
110
|
private requireAuth;
|
|
111
|
+
/**
|
|
112
|
+
* Throws {@link MacpSdkError}, without sending anything, unless
|
|
113
|
+
* `capabilities.<group>.<flag>` from {@link initialize} is `true`. Calls
|
|
114
|
+
* `Initialize` once if this client hasn't; concurrent first callers share
|
|
115
|
+
* that one in-flight call (and the first caller's deadline).
|
|
116
|
+
*/
|
|
117
|
+
private requireCapability;
|
|
118
|
+
/**
|
|
119
|
+
* The single metadata choke point: auth, plus W3C trace context
|
|
120
|
+
* (RFC-MACP-0006 §2.1) when `@opentelemetry/api` is installed and a valid
|
|
121
|
+
* span context is active — or, for Send, the send span's own context.
|
|
122
|
+
*/
|
|
108
123
|
private metadata;
|
|
109
124
|
private deadline;
|
|
110
125
|
private unary;
|
|
@@ -249,6 +264,45 @@ export declare class MacpClient {
|
|
|
249
264
|
auth?: AuthConfig;
|
|
250
265
|
deadlineMs?: number;
|
|
251
266
|
}): Promise<WhatIfReplayResult>;
|
|
267
|
+
/**
|
|
268
|
+
* Fetch the runtime-issued decision receipt for a resolved session
|
|
269
|
+
* (RFC-MACP-0013 §13). Returns the parsed `macp-receipt/1` object and `raw`,
|
|
270
|
+
* the exact JSON text the runtime served, which is stable across calls
|
|
271
|
+
* (§13 item 6), so it can be stored or forwarded byte for byte. Verify it
|
|
272
|
+
* with `verifyReceipt(receipt, trustedKeysFromJwks(await getReceiptKeys()))`;
|
|
273
|
+
* this method does not.
|
|
274
|
+
*
|
|
275
|
+
* The RPC is optional, so this checks
|
|
276
|
+
* `capabilities.decisionReceipt.getDecisionReceipt` from {@link initialize}
|
|
277
|
+
* first (calling it once if this client hasn't) and throws
|
|
278
|
+
* {@link MacpSdkError} without sending anything when the runtime doesn't
|
|
279
|
+
* advertise it. Authorized as {@link getSession}. Runtime refusals arrive as
|
|
280
|
+
* {@link MacpTransportError} with `macpErrorCode` set: `RECEIPT_UNAVAILABLE`
|
|
281
|
+
* (no receipt can be issued: not resolved yet, or resolved without a
|
|
282
|
+
* `CommitmentPayload`), `SESSION_NOT_FOUND`, `FORBIDDEN`,
|
|
283
|
+
* `INTERNAL_ERROR`. A `receipt_json` that isn't a JSON object throws
|
|
284
|
+
* {@link MacpSdkError}.
|
|
285
|
+
*/
|
|
286
|
+
getDecisionReceipt(sessionId: string, options?: {
|
|
287
|
+
auth?: AuthConfig;
|
|
288
|
+
deadlineMs?: number;
|
|
289
|
+
}): Promise<DecisionReceiptResult>;
|
|
290
|
+
/**
|
|
291
|
+
* Fetch the JWK Set of public keys the runtime signs receipts with
|
|
292
|
+
* (RFC-MACP-0013 §13 item 8); an unsigned runtime returns `{keys: []}`. Pass
|
|
293
|
+
* it to `trustedKeysFromJwks` for the map `verifyReceipt` takes. Whether
|
|
294
|
+
* to trust these keys is the caller's decision.
|
|
295
|
+
*
|
|
296
|
+
* Gated on `capabilities.decisionReceipt.getDecisionReceipt` like
|
|
297
|
+
* {@link getDecisionReceipt}. Needs transport auth only, like
|
|
298
|
+
* {@link getManifest}: `auth` is optional and falls back to the client's.
|
|
299
|
+
* A `jwks_json` that isn't a JSON object with a `keys` array throws
|
|
300
|
+
* {@link MacpSdkError}.
|
|
301
|
+
*/
|
|
302
|
+
getReceiptKeys(options?: {
|
|
303
|
+
auth?: AuthConfig;
|
|
304
|
+
deadlineMs?: number;
|
|
305
|
+
}): Promise<ReceiptKeySet>;
|
|
252
306
|
/**
|
|
253
307
|
* Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
|
|
254
308
|
* lets the server choose the page size; the server MAY cap it. Callers MUST
|
package/dist/client.js
CHANGED
|
@@ -47,6 +47,7 @@ const path = __importStar(require("node:path"));
|
|
|
47
47
|
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
48
48
|
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
49
49
|
const auth_1 = require("./auth");
|
|
50
|
+
const otel_1 = require("./otel");
|
|
50
51
|
const constants_1 = require("./constants");
|
|
51
52
|
const envelope_1 = require("./envelope");
|
|
52
53
|
const errors_1 = require("./errors");
|
|
@@ -276,8 +277,10 @@ class MacpClient {
|
|
|
276
277
|
defaultDeadlineMs;
|
|
277
278
|
clientName;
|
|
278
279
|
clientVersion;
|
|
279
|
-
/** The last {@link initialize} result;
|
|
280
|
+
/** The last {@link initialize} result; capability-gated RPCs read its capabilities. */
|
|
280
281
|
initializeResult;
|
|
282
|
+
/** An `Initialize` that {@link requireCapability} started and is still in flight. */
|
|
283
|
+
pendingInit;
|
|
281
284
|
/** The runtime's capabilities from the last {@link initialize}; `undefined` before one. */
|
|
282
285
|
get serverCapabilities() {
|
|
283
286
|
return this.initializeResult?.capabilities;
|
|
@@ -320,17 +323,44 @@ class MacpClient {
|
|
|
320
323
|
throw new errors_1.MacpSdkError('this operation requires auth; pass auth= or configure client.auth');
|
|
321
324
|
return selected;
|
|
322
325
|
}
|
|
323
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Throws {@link MacpSdkError}, without sending anything, unless
|
|
328
|
+
* `capabilities.<group>.<flag>` from {@link initialize} is `true`. Calls
|
|
329
|
+
* `Initialize` once if this client hasn't; concurrent first callers share
|
|
330
|
+
* that one in-flight call (and the first caller's deadline).
|
|
331
|
+
*/
|
|
332
|
+
async requireCapability(group, flag, rpc, rfc, deadlineMs) {
|
|
333
|
+
const capabilities = this.serverCapabilities ??
|
|
334
|
+
(await (this.pendingInit ??= this.initialize(deadlineMs).finally(() => {
|
|
335
|
+
this.pendingInit = undefined;
|
|
336
|
+
}))).capabilities;
|
|
337
|
+
const advertised = capabilities?.[group];
|
|
338
|
+
if (advertised?.[flag] !== true) {
|
|
339
|
+
throw new errors_1.MacpSdkError(`this runtime does not advertise capabilities.${group}.${flag}; ` +
|
|
340
|
+
`${rpc} is optional and a client MUST NOT call it otherwise (${rfc})`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The single metadata choke point: auth, plus W3C trace context
|
|
345
|
+
* (RFC-MACP-0006 §2.1) when `@opentelemetry/api` is installed and a valid
|
|
346
|
+
* span context is active — or, for Send, the send span's own context.
|
|
347
|
+
*/
|
|
348
|
+
metadata(auth, traceContext) {
|
|
324
349
|
const selected = auth ?? this.auth;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
350
|
+
const metadata = selected ? (0, auth_1.metadataFromAuth)(selected) : undefined;
|
|
351
|
+
const headers = (0, otel_1.traceHeaders)(traceContext);
|
|
352
|
+
if (headers.length === 0)
|
|
353
|
+
return metadata;
|
|
354
|
+
const withTrace = metadata ?? new grpc.Metadata();
|
|
355
|
+
for (const [key, value] of headers)
|
|
356
|
+
withTrace.set(key, value);
|
|
357
|
+
return withTrace;
|
|
328
358
|
}
|
|
329
359
|
deadline(deadlineMs) {
|
|
330
360
|
const resolved = deadlineMs ?? this.defaultDeadlineMs;
|
|
331
361
|
return resolved ? new Date(Date.now() + resolved) : undefined;
|
|
332
362
|
}
|
|
333
|
-
unary(method, request, auth, deadlineMs, ackStatuses, registryCodeAsAck = false) {
|
|
363
|
+
unary(method, request, auth, deadlineMs, ackStatuses, registryCodeAsAck = false, traceContext) {
|
|
334
364
|
return new Promise((resolve, reject) => {
|
|
335
365
|
const callback = (error, response) => {
|
|
336
366
|
if (error) {
|
|
@@ -354,7 +384,7 @@ class MacpClient {
|
|
|
354
384
|
}
|
|
355
385
|
};
|
|
356
386
|
const deadline = this.deadline(deadlineMs);
|
|
357
|
-
const metadata = this.metadata(auth);
|
|
387
|
+
const metadata = this.metadata(auth, traceContext);
|
|
358
388
|
if (metadata && deadline)
|
|
359
389
|
this.client[method](request, metadata, { deadline }, callback);
|
|
360
390
|
else if (metadata)
|
|
@@ -391,14 +421,30 @@ class MacpClient {
|
|
|
391
421
|
}
|
|
392
422
|
async send(envelope, options) {
|
|
393
423
|
const auth = this.requireAuth(options?.auth);
|
|
394
|
-
|
|
424
|
+
// A malformed credential fails here, before the span starts: a send that
|
|
425
|
+
// never reaches the wire emits no span.
|
|
426
|
+
(0, auth_1.validateAuth)(auth);
|
|
427
|
+
// One CLIENT span per call (undefined without @opentelemetry/api); its
|
|
428
|
+
// own context goes into the Send metadata so the runtime's server span
|
|
429
|
+
// is its child.
|
|
430
|
+
const span = (0, otel_1.startSendSpan)(envelope);
|
|
431
|
+
const call = () => this.unary('Send', { envelope }, auth, options?.deadlineMs,
|
|
395
432
|
// gRPC ALREADY_EXISTS/FAILED_PRECONDITION on Send are application-level
|
|
396
433
|
// rejections the runtime couldn't express as a normal NACK (issue #107);
|
|
397
434
|
// surface them as MacpAckError, matching macp-sdk-python (client.py:442-452).
|
|
398
435
|
// That status map is the pre-0.9.0 fallback: a `macp-error-code` key
|
|
399
436
|
// (runtime ≥ 0.9.0) supplies the exact registry code instead.
|
|
400
|
-
{ ALREADY_EXISTS: constants_1.SESSION_ALREADY_EXISTS, FAILED_PRECONDITION: constants_1.POLICY_DENIED }, true);
|
|
401
|
-
|
|
437
|
+
{ ALREADY_EXISTS: constants_1.SESSION_ALREADY_EXISTS, FAILED_PRECONDITION: constants_1.POLICY_DENIED }, true, span?.context);
|
|
438
|
+
let response;
|
|
439
|
+
try {
|
|
440
|
+
response = await (span ? span.run(call) : call());
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
span?.end({ error });
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
const ack = response?.ack;
|
|
447
|
+
span?.end({ ack: ack ?? {} });
|
|
402
448
|
// Duplicate acks are success — the message was already accepted
|
|
403
449
|
if (ack?.duplicate)
|
|
404
450
|
return ack;
|
|
@@ -538,12 +584,7 @@ class MacpClient {
|
|
|
538
584
|
*/
|
|
539
585
|
async whatIfReplay(sessionId, candidate, options) {
|
|
540
586
|
const auth = this.requireAuth(options?.auth);
|
|
541
|
-
|
|
542
|
-
const policyRegistry = capabilities?.policyRegistry;
|
|
543
|
-
if (!policyRegistry?.whatIfReplay) {
|
|
544
|
-
throw new errors_1.MacpSdkError('this runtime does not advertise capabilities.policyRegistry.whatIfReplay; ' +
|
|
545
|
-
'WhatIfReplay is optional and a client MUST NOT call it otherwise (RFC-MACP-0012 §8.1)');
|
|
546
|
-
}
|
|
587
|
+
await this.requireCapability('policyRegistry', 'whatIfReplay', 'WhatIfReplay', 'RFC-MACP-0012 §8.1', options?.deadlineMs);
|
|
547
588
|
const res = await this.unary('WhatIfReplay', { sessionId, candidate }, auth, options?.deadlineMs);
|
|
548
589
|
// proto3 `optional` decodes with a synthetic oneof discriminator; an unset
|
|
549
590
|
// first_divergence is absent, never "0" (index 0 is the SessionStart).
|
|
@@ -555,6 +596,54 @@ class MacpClient {
|
|
|
555
596
|
delete result._firstDivergence;
|
|
556
597
|
return result;
|
|
557
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Fetch the runtime-issued decision receipt for a resolved session
|
|
601
|
+
* (RFC-MACP-0013 §13). Returns the parsed `macp-receipt/1` object and `raw`,
|
|
602
|
+
* the exact JSON text the runtime served, which is stable across calls
|
|
603
|
+
* (§13 item 6), so it can be stored or forwarded byte for byte. Verify it
|
|
604
|
+
* with `verifyReceipt(receipt, trustedKeysFromJwks(await getReceiptKeys()))`;
|
|
605
|
+
* this method does not.
|
|
606
|
+
*
|
|
607
|
+
* The RPC is optional, so this checks
|
|
608
|
+
* `capabilities.decisionReceipt.getDecisionReceipt` from {@link initialize}
|
|
609
|
+
* first (calling it once if this client hasn't) and throws
|
|
610
|
+
* {@link MacpSdkError} without sending anything when the runtime doesn't
|
|
611
|
+
* advertise it. Authorized as {@link getSession}. Runtime refusals arrive as
|
|
612
|
+
* {@link MacpTransportError} with `macpErrorCode` set: `RECEIPT_UNAVAILABLE`
|
|
613
|
+
* (no receipt can be issued: not resolved yet, or resolved without a
|
|
614
|
+
* `CommitmentPayload`), `SESSION_NOT_FOUND`, `FORBIDDEN`,
|
|
615
|
+
* `INTERNAL_ERROR`. A `receipt_json` that isn't a JSON object throws
|
|
616
|
+
* {@link MacpSdkError}.
|
|
617
|
+
*/
|
|
618
|
+
async getDecisionReceipt(sessionId, options) {
|
|
619
|
+
const auth = this.requireAuth(options?.auth);
|
|
620
|
+
await this.requireCapability('decisionReceipt', 'getDecisionReceipt', 'GetDecisionReceipt', 'RFC-MACP-0013 §13', options?.deadlineMs);
|
|
621
|
+
const res = await this.unary('GetDecisionReceipt', { sessionId }, auth, options?.deadlineMs);
|
|
622
|
+
const raw = res.receiptJson ?? '';
|
|
623
|
+
const receipt = parseJsonObject(raw, 'receipt_json');
|
|
624
|
+
return { receipt: receipt, raw };
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Fetch the JWK Set of public keys the runtime signs receipts with
|
|
628
|
+
* (RFC-MACP-0013 §13 item 8); an unsigned runtime returns `{keys: []}`. Pass
|
|
629
|
+
* it to `trustedKeysFromJwks` for the map `verifyReceipt` takes. Whether
|
|
630
|
+
* to trust these keys is the caller's decision.
|
|
631
|
+
*
|
|
632
|
+
* Gated on `capabilities.decisionReceipt.getDecisionReceipt` like
|
|
633
|
+
* {@link getDecisionReceipt}. Needs transport auth only, like
|
|
634
|
+
* {@link getManifest}: `auth` is optional and falls back to the client's.
|
|
635
|
+
* A `jwks_json` that isn't a JSON object with a `keys` array throws
|
|
636
|
+
* {@link MacpSdkError}.
|
|
637
|
+
*/
|
|
638
|
+
async getReceiptKeys(options) {
|
|
639
|
+
await this.requireCapability('decisionReceipt', 'getDecisionReceipt', 'GetReceiptKeys', 'RFC-MACP-0013 §13', options?.deadlineMs);
|
|
640
|
+
const res = await this.unary('GetReceiptKeys', {}, options?.auth, options?.deadlineMs);
|
|
641
|
+
const jwks = parseJsonObject(res.jwksJson ?? '', 'jwks_json');
|
|
642
|
+
if (!Array.isArray(jwks.keys)) {
|
|
643
|
+
throw new errors_1.MacpSdkError('runtime returned a jwks_json with no keys array');
|
|
644
|
+
}
|
|
645
|
+
return jwks;
|
|
646
|
+
}
|
|
558
647
|
/**
|
|
559
648
|
* Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
|
|
560
649
|
* lets the server choose the page size; the server MAY cap it. Callers MUST
|
|
@@ -621,7 +710,7 @@ class MacpClient {
|
|
|
621
710
|
*/
|
|
622
711
|
watchSignals(auth) {
|
|
623
712
|
const selected = this.requireAuth(auth);
|
|
624
|
-
const metadata =
|
|
713
|
+
const metadata = this.metadata(selected);
|
|
625
714
|
return this.client.WatchSignals({}, metadata);
|
|
626
715
|
}
|
|
627
716
|
async sendSignal(options) {
|
|
@@ -683,3 +772,17 @@ class MacpClient {
|
|
|
683
772
|
}
|
|
684
773
|
}
|
|
685
774
|
exports.MacpClient = MacpClient;
|
|
775
|
+
/** Parses runtime-served JSON text that must be an object, else throws {@link MacpSdkError}. */
|
|
776
|
+
function parseJsonObject(text, field) {
|
|
777
|
+
let value;
|
|
778
|
+
try {
|
|
779
|
+
value = JSON.parse(text);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
throw new errors_1.MacpSdkError(`runtime returned a ${field} that is not valid JSON: ${err.message}`);
|
|
783
|
+
}
|
|
784
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
785
|
+
throw new errors_1.MacpSdkError(`runtime returned a ${field} that is not a JSON object`);
|
|
786
|
+
}
|
|
787
|
+
return value;
|
|
788
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -42,6 +42,13 @@ export declare const AGENT_NOT_FOUND = "AGENT_NOT_FOUND";
|
|
|
42
42
|
* holds (e.g. compacted when it ended) — FAILED_PRECONDITION. RFC-MACP-0012 §8.1.
|
|
43
43
|
*/
|
|
44
44
|
export declare const HISTORY_UNAVAILABLE = "HISTORY_UNAVAILABLE";
|
|
45
|
+
/**
|
|
46
|
+
* GetDecisionReceipt named a session no receipt can be issued for: not
|
|
47
|
+
* RESOLVED, resolved without a `CommitmentPayload`, or issuance failed —
|
|
48
|
+
* FAILED_PRECONDITION. RFC-MACP-0013 §13. **Provisional** in the spec's error
|
|
49
|
+
* registry (not yet in the parity contract's permanent set).
|
|
50
|
+
*/
|
|
51
|
+
export declare const RECEIPT_UNAVAILABLE = "RECEIPT_UNAVAILABLE";
|
|
45
52
|
export declare const DENY_VETO = "veto";
|
|
46
53
|
export declare const DENY_VETO_HOLD = "veto_hold";
|
|
47
54
|
export declare const DENY_QUORUM_NOT_MET = "quorum_not_met";
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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.HISTORY_UNAVAILABLE = 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;
|
|
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.RECEIPT_UNAVAILABLE = exports.HISTORY_UNAVAILABLE = 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';
|
|
@@ -49,6 +49,13 @@ exports.AGENT_NOT_FOUND = 'AGENT_NOT_FOUND';
|
|
|
49
49
|
* holds (e.g. compacted when it ended) — FAILED_PRECONDITION. RFC-MACP-0012 §8.1.
|
|
50
50
|
*/
|
|
51
51
|
exports.HISTORY_UNAVAILABLE = 'HISTORY_UNAVAILABLE';
|
|
52
|
+
/**
|
|
53
|
+
* GetDecisionReceipt named a session no receipt can be issued for: not
|
|
54
|
+
* RESOLVED, resolved without a `CommitmentPayload`, or issuance failed —
|
|
55
|
+
* FAILED_PRECONDITION. RFC-MACP-0013 §13. **Provisional** in the spec's error
|
|
56
|
+
* registry (not yet in the parity contract's permanent set).
|
|
57
|
+
*/
|
|
58
|
+
exports.RECEIPT_UNAVAILABLE = 'RECEIPT_UNAVAILABLE';
|
|
52
59
|
// Structured POLICY_DENIED denial codes (`PolicyDenial.code`, runtime ≥ 0.9.0;
|
|
53
60
|
// RFC-MACP-0012 §6.2). Open set — names match python-sdk's exports.
|
|
54
61
|
exports.DENY_VETO = 'veto';
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export * from './proposal';
|
|
|
14
14
|
export * from './proto-registry';
|
|
15
15
|
export * from './retry';
|
|
16
16
|
export * from './quorum';
|
|
17
|
-
export { RECEIPT_FORMAT, buildReceipt, commitmentFromProjection, verifyReceipt, type AttestationOutcome, type BuildReceiptOptions, type DecisionReceipt, type JwsAttestation, type ReceiptAttestation, type ReceiptCommitment, type ReceiptIntegrity, type ReceiptSigner, type ReceiptVerdict, type ReceiptVerification, type VendorAttestation, } from './receipt';
|
|
17
|
+
export { RECEIPT_FORMAT, buildReceipt, commitmentFromProjection, trustedKeysFromJwks, verifyReceipt, type AttestationOutcome, type BuildReceiptOptions, type DecisionReceipt, type DecisionReceiptResult, type JwsAttestation, type ReceiptAttestation, type ReceiptCommitment, type ReceiptIntegrity, type ReceiptJwk, type ReceiptKeySet, type ReceiptSigner, type ReceiptVerdict, type ReceiptVerification, type VendorAttestation, } from './receipt';
|
|
18
18
|
export * from './task';
|
|
19
19
|
export * from './types';
|
|
20
20
|
export * from './validation';
|
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.verifyReceipt = exports.commitmentFromProjection = exports.buildReceipt = exports.RECEIPT_FORMAT = exports.logger = exports.configureLogging = exports.verifyCommitmentHash = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
|
|
39
|
+
exports.agent = exports.VERSION = exports.verifyReceipt = exports.trustedKeysFromJwks = exports.commitmentFromProjection = exports.buildReceipt = exports.RECEIPT_FORMAT = 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`
|
|
@@ -84,6 +84,7 @@ var receipt_1 = require("./receipt");
|
|
|
84
84
|
Object.defineProperty(exports, "RECEIPT_FORMAT", { enumerable: true, get: function () { return receipt_1.RECEIPT_FORMAT; } });
|
|
85
85
|
Object.defineProperty(exports, "buildReceipt", { enumerable: true, get: function () { return receipt_1.buildReceipt; } });
|
|
86
86
|
Object.defineProperty(exports, "commitmentFromProjection", { enumerable: true, get: function () { return receipt_1.commitmentFromProjection; } });
|
|
87
|
+
Object.defineProperty(exports, "trustedKeysFromJwks", { enumerable: true, get: function () { return receipt_1.trustedKeysFromJwks; } });
|
|
87
88
|
Object.defineProperty(exports, "verifyReceipt", { enumerable: true, get: function () { return receipt_1.verifyReceipt; } });
|
|
88
89
|
__exportStar(require("./task"), exports);
|
|
89
90
|
__exportStar(require("./types"), exports);
|
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional OpenTelemetry instrumentation (docs/opentelemetry.md in the spec
|
|
3
|
+
* repo, semconv `span.macp.send`; propagation per RFC-MACP-0006 §2.1).
|
|
4
|
+
*
|
|
5
|
+
* `@opentelemetry/api` is an optional peer dependency. It is required lazily,
|
|
6
|
+
* on the first RPC, never at import, so loading the SDK never touches it, and
|
|
7
|
+
* when it is not installed every function here is a no-op. Nothing in this
|
|
8
|
+
* module may throw into the send path: instrumentation must never fail a call.
|
|
9
|
+
*
|
|
10
|
+
* Internal: not exported from `src/index.ts`, and never imported by the
|
|
11
|
+
* browser-safe `./receipt` subpath.
|
|
12
|
+
*/
|
|
13
|
+
import type * as OtelApiModule from '@opentelemetry/api';
|
|
14
|
+
import type { Ack, Envelope } from './types';
|
|
15
|
+
type OtelApi = typeof OtelApiModule;
|
|
16
|
+
/** A span context, opaque outside this module. */
|
|
17
|
+
export type TraceContext = OtelApiModule.SpanContext;
|
|
18
|
+
/**
|
|
19
|
+
* Test seam: `null` forces "not installed", a module object replaces the real
|
|
20
|
+
* one, `undefined` restores normal loading.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setOtelApiForTesting(api: OtelApi | null | undefined): void;
|
|
23
|
+
/**
|
|
24
|
+
* W3C `traceparent`/`tracestate` for the given span context, or the active
|
|
25
|
+
* one when none is given. Built from the API alone rather than through the
|
|
26
|
+
* global propagator, which would also inject `baggage` (arbitrary user data)
|
|
27
|
+
* and follow a user's B3/Jaeger configuration; §2.1 names only these two.
|
|
28
|
+
*/
|
|
29
|
+
export declare function traceHeaders(explicit?: TraceContext): Array<[string, string]>;
|
|
30
|
+
/** One in-flight `send {macp.mode.id}` CLIENT span. */
|
|
31
|
+
export interface SendSpan {
|
|
32
|
+
/** The span's own context, injected into the Send metadata. */
|
|
33
|
+
readonly context: TraceContext | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Run `fn` with this span active, so a transport client span (for example
|
|
36
|
+
* from gRPC auto-instrumentation) is its child, as the semconv note on
|
|
37
|
+
* `span.macp.send` requires. Calls `fn` exactly once; an OpenTelemetry
|
|
38
|
+
* failure falls back to calling it directly.
|
|
39
|
+
*/
|
|
40
|
+
run<T>(fn: () => T): T;
|
|
41
|
+
/** Record the outcome and end the span. Safe to call once; never throws. */
|
|
42
|
+
end(result: {
|
|
43
|
+
ack: Ack;
|
|
44
|
+
} | {
|
|
45
|
+
error: unknown;
|
|
46
|
+
}): void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start the CLIENT span for one `send()`, or return `undefined` when
|
|
50
|
+
* OpenTelemetry is not installed (or the tracer fails). Only identifiers and
|
|
51
|
+
* types are recorded — never payload, `reason`, or any error message.
|
|
52
|
+
*/
|
|
53
|
+
export declare function startSendSpan(envelope: Envelope): SendSpan | undefined;
|
|
54
|
+
export {};
|
package/dist/otel.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setOtelApiForTesting = setOtelApiForTesting;
|
|
4
|
+
exports.traceHeaders = traceHeaders;
|
|
5
|
+
exports.startSendSpan = startSendSpan;
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
const TRACER_NAME = 'macp-sdk-typescript';
|
|
8
|
+
/** OpenTelemetry's fallback `error.type` when no better value is known. */
|
|
9
|
+
const ERROR_TYPE_OTHER = '_OTHER';
|
|
10
|
+
// undefined: not tried yet; null: tried, absent.
|
|
11
|
+
let loaded;
|
|
12
|
+
let override;
|
|
13
|
+
let tracerVersion;
|
|
14
|
+
/**
|
|
15
|
+
* Test seam: `null` forces "not installed", a module object replaces the real
|
|
16
|
+
* one, `undefined` restores normal loading.
|
|
17
|
+
*/
|
|
18
|
+
function setOtelApiForTesting(api) {
|
|
19
|
+
override = api;
|
|
20
|
+
}
|
|
21
|
+
function otelApi() {
|
|
22
|
+
if (override !== undefined)
|
|
23
|
+
return override ?? undefined;
|
|
24
|
+
if (loaded === undefined) {
|
|
25
|
+
try {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
loaded = require('@opentelemetry/api');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
loaded = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return loaded ?? undefined;
|
|
34
|
+
}
|
|
35
|
+
function sdkVersion() {
|
|
36
|
+
if (tracerVersion === undefined) {
|
|
37
|
+
try {
|
|
38
|
+
// Resolves to the package root from both src/ and dist/.
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
tracerVersion = String(require('../package.json').version ?? '');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
tracerVersion = '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return tracerVersion || undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* W3C `traceparent`/`tracestate` for the given span context, or the active
|
|
50
|
+
* one when none is given. Built from the API alone rather than through the
|
|
51
|
+
* global propagator, which would also inject `baggage` (arbitrary user data)
|
|
52
|
+
* and follow a user's B3/Jaeger configuration; §2.1 names only these two.
|
|
53
|
+
*/
|
|
54
|
+
function traceHeaders(explicit) {
|
|
55
|
+
const api = otelApi();
|
|
56
|
+
if (!api)
|
|
57
|
+
return [];
|
|
58
|
+
try {
|
|
59
|
+
const spanContext = explicit ?? api.trace.getSpanContext(api.context.active());
|
|
60
|
+
if (!spanContext || !api.isSpanContextValid(spanContext))
|
|
61
|
+
return [];
|
|
62
|
+
const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, '0');
|
|
63
|
+
const headers = [
|
|
64
|
+
['traceparent', `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`],
|
|
65
|
+
];
|
|
66
|
+
const tracestate = spanContext.traceState?.serialize();
|
|
67
|
+
if (tracestate)
|
|
68
|
+
headers.push(['tracestate', tracestate]);
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start the CLIENT span for one `send()`, or return `undefined` when
|
|
77
|
+
* OpenTelemetry is not installed (or the tracer fails). Only identifiers and
|
|
78
|
+
* types are recorded — never payload, `reason`, or any error message.
|
|
79
|
+
*/
|
|
80
|
+
function startSendSpan(envelope) {
|
|
81
|
+
const api = otelApi();
|
|
82
|
+
if (!api)
|
|
83
|
+
return undefined;
|
|
84
|
+
try {
|
|
85
|
+
const attributes = {
|
|
86
|
+
'macp.operation.name': 'send',
|
|
87
|
+
'macp.message.id': envelope.messageId,
|
|
88
|
+
'macp.message.type': envelope.messageType,
|
|
89
|
+
};
|
|
90
|
+
if (envelope.macpVersion)
|
|
91
|
+
attributes['macp.version'] = envelope.macpVersion;
|
|
92
|
+
if (envelope.sessionId)
|
|
93
|
+
attributes['macp.session.id'] = envelope.sessionId;
|
|
94
|
+
if (envelope.mode)
|
|
95
|
+
attributes['macp.mode.id'] = envelope.mode;
|
|
96
|
+
if (envelope.sender)
|
|
97
|
+
attributes['macp.sender.id'] = envelope.sender;
|
|
98
|
+
const name = envelope.mode ? `send ${envelope.mode}` : 'send';
|
|
99
|
+
const span = api.trace
|
|
100
|
+
.getTracer(TRACER_NAME, sdkVersion())
|
|
101
|
+
.startSpan(name, { kind: api.SpanKind.CLIENT, attributes }, api.context.active());
|
|
102
|
+
let ended = false;
|
|
103
|
+
return {
|
|
104
|
+
context: safe(() => span.spanContext()),
|
|
105
|
+
run(fn) {
|
|
106
|
+
let called = false;
|
|
107
|
+
let returned = false;
|
|
108
|
+
let result;
|
|
109
|
+
const once = () => {
|
|
110
|
+
called = true;
|
|
111
|
+
result = fn();
|
|
112
|
+
returned = true;
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
return api.context.with(api.trace.setSpan(api.context.active(), span), once);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// fn's own throw propagates; an OpenTelemetry failure before or
|
|
120
|
+
// after fn never replaces the call's real result.
|
|
121
|
+
if (!called)
|
|
122
|
+
return fn();
|
|
123
|
+
if (returned)
|
|
124
|
+
return result;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
end(result) {
|
|
129
|
+
if (ended)
|
|
130
|
+
return;
|
|
131
|
+
ended = true;
|
|
132
|
+
safe(() => {
|
|
133
|
+
recordOutcome(api, span, result);
|
|
134
|
+
});
|
|
135
|
+
safe(() => span.end());
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function recordOutcome(api, span, result) {
|
|
144
|
+
if ('ack' in result) {
|
|
145
|
+
const { ack } = result;
|
|
146
|
+
if (ack.duplicate)
|
|
147
|
+
span.setAttribute('macp.envelope.outcome', 'duplicate');
|
|
148
|
+
else if (ack.ok)
|
|
149
|
+
span.setAttribute('macp.envelope.outcome', 'accepted');
|
|
150
|
+
else
|
|
151
|
+
rejected(span, ack.error?.code);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const { error } = result;
|
|
155
|
+
// A rejection is a normal protocol outcome: status stays UNSET. The SDK
|
|
156
|
+
// raises MacpAckError for a NACK, a status carrying `macp-error-code`, and
|
|
157
|
+
// the pre-0.9 status fallback — the span classifies exactly as the caller's
|
|
158
|
+
// exception does.
|
|
159
|
+
if (error instanceof errors_1.MacpAckError) {
|
|
160
|
+
rejected(span, error.failure.code);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const errorType = (error instanceof errors_1.MacpTransportError ? (error.macpErrorCode ?? error.code) : undefined) || ERROR_TYPE_OTHER;
|
|
164
|
+
span.setAttribute('error.type', errorType);
|
|
165
|
+
// The description is the code only; exception messages are never recorded.
|
|
166
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: errorType });
|
|
167
|
+
}
|
|
168
|
+
function rejected(span, code) {
|
|
169
|
+
span.setAttribute('macp.envelope.outcome', 'rejected');
|
|
170
|
+
span.setAttribute('error.type', code || ERROR_TYPE_OTHER);
|
|
171
|
+
}
|
|
172
|
+
function safe(fn) {
|
|
173
|
+
try {
|
|
174
|
+
return fn();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/receipt.d.ts
CHANGED
|
@@ -49,6 +49,32 @@ export interface DecisionReceipt {
|
|
|
49
49
|
issued_at: string;
|
|
50
50
|
attestations: ReceiptAttestation[];
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* {@link MacpClient.getDecisionReceipt}'s result: the parsed receipt and the
|
|
54
|
+
* `receipt_json` text exactly as the runtime served it, so a caller can store
|
|
55
|
+
* the issued bytes unchanged (RFC-MACP-0013 §13 item 6 serves them
|
|
56
|
+
* byte-identical). `receipt` is parsed, not validated: {@link verifyReceipt}
|
|
57
|
+
* is the authority on whether it is a well-formed `macp-receipt/1`.
|
|
58
|
+
*/
|
|
59
|
+
export interface DecisionReceiptResult {
|
|
60
|
+
receipt: DecisionReceipt;
|
|
61
|
+
raw: string;
|
|
62
|
+
}
|
|
63
|
+
/** One JWK of a receipt key set (`schemas/json/macp-receipt-keys.schema.json`). */
|
|
64
|
+
export interface ReceiptJwk {
|
|
65
|
+
kid: string;
|
|
66
|
+
kty: string;
|
|
67
|
+
crv?: string;
|
|
68
|
+
x?: string;
|
|
69
|
+
alg?: string;
|
|
70
|
+
use?: string;
|
|
71
|
+
[member: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
/** A JWK Set [RFC 7517] of receipt verification keys, as `GetReceiptKeys` returns it. */
|
|
74
|
+
export interface ReceiptKeySet {
|
|
75
|
+
keys: ReceiptJwk[];
|
|
76
|
+
[member: string]: unknown;
|
|
77
|
+
}
|
|
52
78
|
/**
|
|
53
79
|
* Signs a receipt with Ed25519: either a 32-byte private key seed, or a
|
|
54
80
|
* callback that returns the 64-byte signature over `signingInput` (for a key
|
|
@@ -122,3 +148,19 @@ export declare function buildReceipt(commitment: CommitmentPayload, options: Bui
|
|
|
122
148
|
* Never throws: malformed input yields an outcome, not an exception.
|
|
123
149
|
*/
|
|
124
150
|
export declare function verifyReceipt(receipt: unknown, trustedKeys: Record<string, string>): ReceiptVerification;
|
|
151
|
+
/**
|
|
152
|
+
* Turns a receipt key set (the JWK Set `GetReceiptKeys` returns, RFC-MACP-0013
|
|
153
|
+
* §13 item 8) into the `kid → x` map {@link verifyReceipt} takes. Choosing to
|
|
154
|
+
* trust the set is the caller's decision; this only converts it.
|
|
155
|
+
*
|
|
156
|
+
* A key is kept only if it is an Ed25519 public key (`kty` `OKP`, `crv`
|
|
157
|
+
* `Ed25519`) with a non-empty `kid` and `x`, an `alg` (if any) of `EdDSA`, a
|
|
158
|
+
* `use` (if any) of `sig`, and no private member (`d`, `p`, `q`, `dp`, `dq`,
|
|
159
|
+
* `qi`, `oth`, `k`): a key set that publishes private key material is
|
|
160
|
+
* misconfigured and is not trusted silently. A `kid` that more than one entry
|
|
161
|
+
* of the set claims is dropped entirely, even when the other entry is a key
|
|
162
|
+
* this function can't use (an EC key, say): item 8 makes kids unique across
|
|
163
|
+
* the whole set, so a repeated one leaves the set ambiguous about which key it
|
|
164
|
+
* names. Never throws: input that is not a key set yields `{}`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function trustedKeysFromJwks(jwks: unknown): Record<string, string>;
|
package/dist/receipt.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.RECEIPT_FORMAT = void 0;
|
|
|
4
4
|
exports.commitmentFromProjection = commitmentFromProjection;
|
|
5
5
|
exports.buildReceipt = buildReceipt;
|
|
6
6
|
exports.verifyReceipt = verifyReceipt;
|
|
7
|
+
exports.trustedKeysFromJwks = trustedKeysFromJwks;
|
|
7
8
|
/**
|
|
8
9
|
* RFC-MACP-0013 Appendix A decision receipts (`macp-receipt/1`).
|
|
9
10
|
*
|
|
@@ -374,3 +375,54 @@ function verifyReceipt(receipt, trustedKeys) {
|
|
|
374
375
|
verdict: outcomes.includes('valid') ? 'attested' : 'integrity_only',
|
|
375
376
|
};
|
|
376
377
|
}
|
|
378
|
+
/** JWK private-key members (RFC 7518 §6); RFC-MACP-0013 §13 item 8 forbids them in a key set. */
|
|
379
|
+
const PRIVATE_JWK_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'];
|
|
380
|
+
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
381
|
+
/** An Ed25519 public verification key usable for an EdDSA `jws` attestation. */
|
|
382
|
+
function usableEd25519Key(key) {
|
|
383
|
+
if (!isRecord(key))
|
|
384
|
+
return false;
|
|
385
|
+
if (key.kty !== 'OKP' || key.crv !== 'Ed25519')
|
|
386
|
+
return false;
|
|
387
|
+
if (!isNonEmptyString(key.kid) || !isNonEmptyString(key.x))
|
|
388
|
+
return false;
|
|
389
|
+
if (key.alg !== undefined && key.alg !== 'EdDSA')
|
|
390
|
+
return false;
|
|
391
|
+
if (key.use !== undefined && key.use !== 'sig')
|
|
392
|
+
return false;
|
|
393
|
+
return !PRIVATE_JWK_MEMBERS.some((m) => Object.prototype.hasOwnProperty.call(key, m));
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Turns a receipt key set (the JWK Set `GetReceiptKeys` returns, RFC-MACP-0013
|
|
397
|
+
* §13 item 8) into the `kid → x` map {@link verifyReceipt} takes. Choosing to
|
|
398
|
+
* trust the set is the caller's decision; this only converts it.
|
|
399
|
+
*
|
|
400
|
+
* A key is kept only if it is an Ed25519 public key (`kty` `OKP`, `crv`
|
|
401
|
+
* `Ed25519`) with a non-empty `kid` and `x`, an `alg` (if any) of `EdDSA`, a
|
|
402
|
+
* `use` (if any) of `sig`, and no private member (`d`, `p`, `q`, `dp`, `dq`,
|
|
403
|
+
* `qi`, `oth`, `k`): a key set that publishes private key material is
|
|
404
|
+
* misconfigured and is not trusted silently. A `kid` that more than one entry
|
|
405
|
+
* of the set claims is dropped entirely, even when the other entry is a key
|
|
406
|
+
* this function can't use (an EC key, say): item 8 makes kids unique across
|
|
407
|
+
* the whole set, so a repeated one leaves the set ambiguous about which key it
|
|
408
|
+
* names. Never throws: input that is not a key set yields `{}`.
|
|
409
|
+
*/
|
|
410
|
+
function trustedKeysFromJwks(jwks) {
|
|
411
|
+
const result = {};
|
|
412
|
+
if (!isRecord(jwks) || !Array.isArray(jwks.keys))
|
|
413
|
+
return result;
|
|
414
|
+
// Count every entry's string kid, usable or not, before keeping any.
|
|
415
|
+
const claims = new Map();
|
|
416
|
+
for (const key of jwks.keys) {
|
|
417
|
+
if (isRecord(key) && typeof key.kid === 'string')
|
|
418
|
+
claims.set(key.kid, (claims.get(key.kid) ?? 0) + 1);
|
|
419
|
+
}
|
|
420
|
+
for (const key of jwks.keys) {
|
|
421
|
+
if (!usableEd25519Key(key) || claims.get(key.kid) !== 1)
|
|
422
|
+
continue;
|
|
423
|
+
// defineProperty, not assignment: a kid of "__proto__" must become an own
|
|
424
|
+
// key (which verifyReceipt's hasOwnProperty lookup finds), not a prototype.
|
|
425
|
+
Object.defineProperty(result, key.kid, { value: key.x, enumerable: true, writable: true, configurable: true });
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -107,6 +107,23 @@ export interface PolicyRegistryCapability {
|
|
|
107
107
|
*/
|
|
108
108
|
whatIfReplay?: boolean;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* `capabilities.decisionReceipt` (`macp.v1.DecisionReceiptCapability`, proto
|
|
112
|
+
* ≥ 0.1.13): optional runtime-issued decision receipts (RFC-MACP-0013 §13).
|
|
113
|
+
*/
|
|
114
|
+
export interface DecisionReceiptCapability {
|
|
115
|
+
/**
|
|
116
|
+
* The runtime implements `GetDecisionReceipt` and `GetReceiptKeys`;
|
|
117
|
+
* {@link MacpClient.getDecisionReceipt} and {@link MacpClient.getReceiptKeys}
|
|
118
|
+
* refuse to call them otherwise.
|
|
119
|
+
*/
|
|
120
|
+
getDecisionReceipt?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Every receipt issued from now on carries at least one EdDSA `jws`
|
|
123
|
+
* attestation. Says nothing about receipts issued earlier.
|
|
124
|
+
*/
|
|
125
|
+
signed?: boolean;
|
|
126
|
+
}
|
|
110
127
|
/** How one replay handled one history entry (`macp.v1.WhatIfEntryOutcome`). */
|
|
111
128
|
export interface WhatIfEntryOutcome {
|
|
112
129
|
kind: 'KIND_UNSPECIFIED' | 'KIND_APPLIED' | 'KIND_NOT_OPEN' | 'KIND_REFUSED' | 'KIND_NO_SESSION';
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "TypeScript SDK for the MACP Rust runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -60,12 +60,23 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@grpc/grpc-js": "^1.14.3",
|
|
62
62
|
"@grpc/proto-loader": "^0.8.0",
|
|
63
|
-
"@multiagentcoordinationprotocol/proto": "^0.1.
|
|
63
|
+
"@multiagentcoordinationprotocol/proto": "^0.1.13",
|
|
64
64
|
"@noble/curves": "^1.9.7",
|
|
65
65
|
"@noble/hashes": "^1.8.0",
|
|
66
66
|
"protobufjs": "^8.6.5"
|
|
67
67
|
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"@opentelemetry/api": ">=1.3.0 <2"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"@opentelemetry/api": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
68
76
|
"devDependencies": {
|
|
77
|
+
"@opentelemetry/api": "~1.9.1",
|
|
78
|
+
"@opentelemetry/context-async-hooks": "^2.12.0",
|
|
79
|
+
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
69
80
|
"@types/node": "^26.4.0",
|
|
70
81
|
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
71
82
|
"@typescript-eslint/parser": "^8.68.0",
|