@metamynd/agentsafe-signer 0.19.1 → 0.20.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/daemon.mjs CHANGED
@@ -6,7 +6,7 @@ import net from 'node:net';
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import crypto from 'node:crypto';
9
- import { buildAuthMessage, buildCheckpointAnchorMessage, buildLocalDecisionMessage } from './policy-core.mjs';
9
+ import { buildAgentSettleMessage, buildAuthMessage, buildCheckpointAnchorMessage, buildLocalDecisionMessage } from './policy-core.mjs';
10
10
  import { envelopeHashFor } from './governance-envelope.mjs';
11
11
  import { buildPayloadBindingMessage, buildPayloadRebindMessage, isPayloadDigest } from './payload-binding.mjs';
12
12
  import { KeyStore, KeyStoreError } from './keystore.mjs';
@@ -18,7 +18,7 @@ export const MAX_LINE_BYTES = 64 * 1024;
18
18
  export const FRESHNESS_MS = 5 * 60 * 1000;
19
19
  export const CLOCK_SKEW_MS = 30 * 1000;
20
20
 
21
- const ALL_SIGNING_OPS = new Set(['sign-authorize', 'sign-envelope', 'sign-payload', 'sign-handshake-nonce', 'sign-key-control-challenge', 'sign-log-checkpoint', 'sign-local-decision', 'sign-service-call', 'get-identity', 'ping']);
21
+ const ALL_SIGNING_OPS = new Set(['sign-authorize', 'sign-envelope', 'sign-payload', 'sign-settle', 'sign-handshake-nonce', 'sign-key-control-challenge', 'sign-log-checkpoint', 'sign-local-decision', 'sign-service-call', 'get-identity', 'ping']);
22
22
 
23
23
  // sign-log-checkpoint is in BOTH role sets, unlike every other sign-* op: it authenticates the
24
24
  // daemon's own log-tamper-evidence checkpoint (T11), which every daemon keeps regardless of
@@ -28,7 +28,7 @@ const ALL_SIGNING_OPS = new Set(['sign-authorize', 'sign-envelope', 'sign-payloa
28
28
  // (agentsafe-guard.mjs's reportLocalDecision()); a service-role daemon never evaluates a
29
29
  // mandate locally, so it has nothing to report.
30
30
  const SIGNING_OPS_BY_ROLE = {
31
- agent: new Set(['sign-authorize', 'sign-envelope', 'sign-payload', 'sign-handshake-nonce', 'sign-key-control-challenge', 'sign-log-checkpoint', 'sign-local-decision', 'get-identity', 'ping']),
31
+ agent: new Set(['sign-authorize', 'sign-envelope', 'sign-payload', 'sign-settle', 'sign-handshake-nonce', 'sign-key-control-challenge', 'sign-log-checkpoint', 'sign-local-decision', 'get-identity', 'ping']),
32
32
  // sign-service-call is SERVICE-role only: a Service's own settlement-surface call (claim / capture / void / …,
33
33
  // MAGP §8.7.6). An agent never claims or settles its own holds — an agent-role daemon signing one would let an
34
34
  // agent impersonate the counterparty that executes for it.
@@ -40,6 +40,8 @@ const DEFAULT_RATE_LIMITS = {
40
40
  'sign-envelope': { max: 20, windowMs: 10_000 },
41
41
  // One per bound authorize, plus one per late binding (a modified hold): never more than sign-authorize itself.
42
42
  'sign-payload': { max: 20, windowMs: 10_000 },
43
+ // An agent settling its OWN unclaimed hold (capture or void): at most one per authorize.
44
+ 'sign-settle': { max: 20, windowMs: 10_000 },
43
45
  'sign-handshake-nonce': { max: 10, windowMs: 60_000 },
44
46
  'sign-key-control-challenge': { max: 3, windowMs: 60 * 60 * 1000 },
45
47
  // Local decisions are reported for every locally-decided block/escalate/non-value-allow, not
@@ -56,6 +58,9 @@ const DEFAULT_RATE_LIMITS = {
56
58
  'sign-service-call': { max: 120, windowMs: 10_000 },
57
59
  };
58
60
 
61
+ // MAGP §8.7.4 — an agent's own unclaimed settlement: capture signs amountCharged, bookingRef, settlementTxHash; void, reason.
62
+ const SETTLE_FIELD_COUNTS = { capture: 3, void: 1 };
63
+
59
64
  // MAGP §8.7.6 — the issuer's own layout (backend counterparty-auth.ts buildCounterpartyMessage):
60
65
  // MAGP-SERVICE-v1 | action | authorizationId | ...fields | nonce | issuedAt (each field "\" and "|" escaped)
61
66
  // The per-action field counts are the ones the issuer reconstructs the message from; anything else could
@@ -269,6 +274,8 @@ export class SignerDaemon {
269
274
  return this.#handleSignEnvelope(params);
270
275
  case 'sign-payload':
271
276
  return this.#handleSignPayload(params);
277
+ case 'sign-settle':
278
+ return this.#handleSignSettle(params);
272
279
  case 'sign-handshake-nonce':
273
280
  return this.#handleSignHandshakeNonce(params);
274
281
  case 'sign-key-control-challenge':
@@ -381,6 +388,30 @@ export class SignerDaemon {
381
388
  return { signature: this.#sign(Buffer.from(message, 'utf8')) };
382
389
  }
383
390
 
391
+ /**
392
+ * An agent settling its OWN hold that nobody has claimed (MAGP §8.7.4): capture it, or void it. The issuer accepts an
393
+ * unclaimed settlement only from that hold's agent, a counterparty the owner registered, or — for an open testnet owner —
394
+ * anyone. Built from validated fields under the fixed MAGP-SETTLE-v1 prefix, which no other message this daemon signs
395
+ * starts with; it names the agent, so it is never a counterparty's signature (sign-service-call, MAGP-SERVICE-v1), and an
396
+ * agent cannot use it to claim. Agent-role only.
397
+ */
398
+ #handleSignSettle(params = {}) {
399
+ this.#assertRateLimit('sign-settle');
400
+ const { verb, agentDid, authorizationId, nonce, issuedAt, fields } = params;
401
+ if (!Object.hasOwn(SETTLE_FIELD_COUNTS, verb)) throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'verb must be capture|void');
402
+ if (typeof agentDid !== 'string' || typeof nonce !== 'string' || typeof issuedAt !== 'string') throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'missing required field');
403
+ if (nonce.length === 0 || nonce.length > 128) throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'invalid nonce');
404
+ if (typeof authorizationId !== 'string' || !UUID_RE.test(authorizationId)) throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'authorizationId must be a UUID');
405
+ if (!Array.isArray(fields) || fields.length !== SETTLE_FIELD_COUNTS[verb] || !fields.every((v) => typeof v === 'string' && v.length <= 512)) {
406
+ throw new DaemonError('DAEMON_MALFORMED_REQUEST', `fields must be ${SETTLE_FIELD_COUNTS[verb]} string(s) for ${verb}`);
407
+ }
408
+ if (verb === 'capture' && !isFiniteNonNegative(Number(fields[0]))) throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'amountCharged must be finite and non-negative');
409
+ this.#assertIdentity(agentDid);
410
+ if (!freshnessOk(issuedAt)) throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'issuedAt outside freshness window');
411
+ const message = buildAgentSettleMessage({ verb, agentDid, authorizationId, nonce, issuedAt, fields });
412
+ return { signature: this.#sign(Buffer.from(message, 'utf8')) };
413
+ }
414
+
384
415
  /**
385
416
  * A Service's own settlement-surface call, signed as its identity (MAGP §8.7.6): the claim that makes it the one
386
417
  * party allowed to settle the hold, and the capture / void / unknown / reconcile that follow. Without this a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamynd/agentsafe-signer",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Local signer daemon for AgentSafe agent/service keys \u2014 the key never enters the calling guard's own process. See docs/design/agent-key-custody-local-signer-daemon-plan.md.",
5
5
  "type": "module",
6
6
  "main": "./daemon.mjs",
package/policy-core.mjs CHANGED
@@ -409,6 +409,11 @@ function requiredContextFor(predicates) {
409
409
  return [...fields].sort();
410
410
  }
411
411
 
412
+ // src/policy-core/own-entry.ts
413
+ function ownEntry(table, key) {
414
+ return typeof key === "string" && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : void 0;
415
+ }
416
+
412
417
  // src/policy-core/standards-rules.ts
413
418
  var CONTEXT_UNVERIFIABLE = "CONTEXT_UNVERIFIABLE";
414
419
  var JURISDICTION_ATOM = "jurisdiction-not-allowed";
@@ -417,7 +422,7 @@ function documentEnforcesJurisdiction(doc) {
417
422
  }
418
423
  var PRECEDENCE = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5, decommission: 6 };
419
424
  function atomFires(atom, ctx) {
420
- const pred = ATOM_REGISTRY[atom.predicate];
425
+ const pred = ownEntry(ATOM_REGISTRY, atom.predicate);
421
426
  if (!pred) return false;
422
427
  try {
423
428
  return !!pred(ctx, atom.config);
@@ -554,7 +559,7 @@ function validateMolecules(molecules) {
554
559
  }
555
560
  }
556
561
  for (const a of m.atoms ?? []) {
557
- if (!ATOM_REGISTRY[a.predicate]) {
562
+ if (!ownEntry(ATOM_REGISTRY, a.predicate)) {
558
563
  issues.push({ moleculeId: m.id, message: `unknown atom predicate '${a.predicate}'` });
559
564
  continue;
560
565
  }
@@ -602,10 +607,10 @@ function reasonFor(constraint, req) {
602
607
  const v = req.values[leftOperand];
603
608
  return v === void 0 || v === null || v === "" ? "JURISDICTION_REQUIRED" : "JURISDICTION_NOT_ALLOWED";
604
609
  }
605
- return REASON_BY_OPERAND[leftOperand] ?? `CONSTRAINT_FAILED:${leftOperand}`;
610
+ return ownEntry(REASON_BY_OPERAND, leftOperand) ?? `CONSTRAINT_FAILED:${leftOperand}`;
606
611
  }
607
612
  function constraintSatisfied(c, req, strict) {
608
- const op = OPERATORS[c.operator];
613
+ const op = ownEntry(OPERATORS, c.operator);
609
614
  if (!op) return false;
610
615
  const left = Object.prototype.hasOwnProperty.call(req.values, c.leftOperand) ? req.values[c.leftOperand] : void 0;
611
616
  if (!c.unit) return op(left, c.rightOperand);
@@ -727,6 +732,10 @@ function buildLegacyAuthMessageV1(f) {
727
732
  function buildLocalDecisionMessage(f) {
728
733
  return [f.agentDid, f.action, f.decision, f.reasonCode, f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
729
734
  }
735
+ var AGENT_SETTLE_PREFIX = "MAGP-SETTLE-v1";
736
+ function buildAgentSettleMessage(f) {
737
+ return [AGENT_SETTLE_PREFIX, f.verb, f.agentDid, f.authorizationId, f.nonce, f.issuedAt, ...f.fields].map((v) => escapeField(String(v))).join("|");
738
+ }
730
739
 
731
740
  // src/policy-core/checkpoint-anchor.ts
732
741
  function buildCheckpointAnchorMessage(f) {
@@ -777,6 +786,7 @@ function operatingModeGate(mode, ctx) {
777
786
  }
778
787
  }
779
788
  export {
789
+ AGENT_SETTLE_PREFIX,
780
790
  ATOM_DEFAULT_REQUIRED_CONTEXT,
781
791
  ATOM_REGISTRY,
782
792
  ATOM_SPECS,
@@ -796,6 +806,7 @@ export {
796
806
  applySignedLast,
797
807
  asOperatingMode,
798
808
  authorityFailure,
809
+ buildAgentSettleMessage,
799
810
  buildAuthMessage,
800
811
  buildCheckpointAnchorMessage,
801
812
  buildLegacyAuthMessageV1,