@metamynd/agentsafe-signer 0.17.1 → 0.18.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/README.md CHANGED
@@ -197,6 +197,18 @@ as the counterparty that executes for it. Rate limit: 120 per 10 s. Needs `@meta
197
197
  (`daemon-log-integrity.smoke.mjs`, above) — though it covers only the local hash-chain half of
198
198
  T11, not the external-anchoring half, which remains open per the bullet above.
199
199
 
200
+ ## Signed jurisdiction (`sign-authorize`, since 0.18.0)
201
+
202
+ `sign-authorize` accepts an optional `jurisdiction` (two ASCII letters; the client normalises it to upper case and
203
+ sends the same string). With it the daemon signs the v2 message (MAGP §8.3.12: the eight fields, `MAGP-AUTH-v2`, the
204
+ jurisdiction) and **echoes** `jurisdiction` in its result; without it, the v1 message exactly as before and no echo.
205
+ A malformed value is `DAEMON_MALFORMED_REQUEST` and nothing is signed. The echo is how a client tells this daemon
206
+ from an older one, which ignores the field and would sign v1 for a request that is sent with it:
207
+ `@metamynd/agentsafe-guard` ≥ 0.16.0 and `metamynd-client` ≥ 0.6.0 refuse such a daemon
208
+ (`JURISDICTION_SIGNING_UNSUPPORTED`) rather than send a request the gate would reject. At the gate, a registered
209
+ payee's country wins over the signed one (`JURISDICTION_MISMATCH`); the other refusals are `JURISDICTION_REQUIRED`
210
+ and `JURISDICTION_NOT_ALLOWED`.
211
+
200
212
  ## Hardening tiers — what a real host actually confirms (T3, T5)
201
213
 
202
214
  `daemon-hardening.smoke.mjs` checks the tier directives **from outside** the process, against the
package/daemon.mjs CHANGED
@@ -300,12 +300,20 @@ export class SignerDaemon {
300
300
  #handleSignAuthorize(params = {}) {
301
301
  this.#assertRateLimit('sign-authorize');
302
302
  this.#validateCoreFields(params);
303
- const { agentDid, action, amount, currency = 'USD', merchant, resource, nonce, issuedAt } = params;
303
+ const { agentDid, action, amount, currency = 'USD', merchant, resource, nonce, issuedAt, jurisdiction } = params;
304
+ // The signed jurisdiction (spec §8.3.12): present → the v2 message, absent → v1 exactly as before. Signed as the
305
+ // literal value given (the client normalises it and sends the same string), so it must be two ASCII letters — the
306
+ // shape the gate accepts. Echoed back so a client can tell this daemon from one that predates the field (which
307
+ // ignores it and signs v1): signed and sent must never differ.
308
+ if (jurisdiction !== undefined && (typeof jurisdiction !== 'string' || !/^[A-Za-z]{2}$/.test(jurisdiction))) {
309
+ throw new DaemonError('DAEMON_MALFORMED_REQUEST', 'jurisdiction must be an ISO 3166-1 alpha-2 code');
310
+ }
304
311
  // `resource` is the 8th canonical field (canonical.ts) — dropping it here (found live: it
305
312
  // was) means every request signed via this daemon silently omits it from the signature,
306
313
  // so the real gate's 8-field reconstruction never matches for any resource-bearing call.
307
- const message = buildAuthMessage({ agentDid, action, amount, currency, merchant, resource, nonce, issuedAt });
308
- return { signature: this.#sign(Buffer.from(message, 'utf8')) };
314
+ const message = buildAuthMessage({ agentDid, action, amount, currency, merchant, resource, nonce, issuedAt, ...(jurisdiction === undefined ? {} : { jurisdiction }) });
315
+ const signature = this.#sign(Buffer.from(message, 'utf8'));
316
+ return jurisdiction === undefined ? { signature } : { signature, jurisdiction };
309
317
  }
310
318
 
311
319
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamynd/agentsafe-signer",
3
- "version": "0.17.1",
3
+ "version": "0.18.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",
@@ -29,7 +29,7 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "scripts": {
32
- "test": "node daemon-protocol.smoke.mjs && node daemon-key-exfiltration.smoke.mjs && node kek-backends.smoke.mjs && node service-installer.smoke.mjs && node daemon-envelope-parity.smoke.mjs && node daemon-admin-socket.smoke.mjs && node daemon-log-integrity.smoke.mjs && node daemon-socket-permissions.smoke.mjs && node daemon-hardening.smoke.mjs && node daemon-hardening-tier2.smoke.mjs && node log-anchor.smoke.mjs && node migrate.smoke.mjs && node secure-memory.smoke.mjs",
32
+ "test": "node daemon-protocol.smoke.mjs && node daemon-key-exfiltration.smoke.mjs && node kek-backends.smoke.mjs && node service-installer.smoke.mjs && node daemon-envelope-parity.smoke.mjs && node daemon-jurisdiction.smoke.mjs && node daemon-admin-socket.smoke.mjs && node daemon-log-integrity.smoke.mjs && node daemon-socket-permissions.smoke.mjs && node daemon-hardening.smoke.mjs && node daemon-hardening-tier2.smoke.mjs && node log-anchor.smoke.mjs && node migrate.smoke.mjs && node secure-memory.smoke.mjs",
33
33
  "start": "node cli.mjs"
34
34
  },
35
35
  "dependencies": {
package/policy-core.mjs CHANGED
@@ -411,6 +411,10 @@ function requiredContextFor(predicates) {
411
411
 
412
412
  // src/policy-core/standards-rules.ts
413
413
  var CONTEXT_UNVERIFIABLE = "CONTEXT_UNVERIFIABLE";
414
+ var JURISDICTION_ATOM = "jurisdiction-not-allowed";
415
+ function documentEnforcesJurisdiction(doc) {
416
+ return (doc?.molecules ?? []).some((m) => m?.decision !== "observe" && (m?.atoms ?? []).some((a) => a?.predicate === JURISDICTION_ATOM));
417
+ }
414
418
  var PRECEDENCE = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5, decommission: 6 };
415
419
  function atomFires(atom, ctx) {
416
420
  const pred = ATOM_REGISTRY[atom.predicate];
@@ -489,7 +493,7 @@ function evaluateBoundStandards(standards, ctx) {
489
493
  const r = evaluateStandardRules(s.document?.molecules, ctx, s.standardKey);
490
494
  if (PRECEDENCE[r.decision] > PRECEDENCE[best.decision]) best = r;
491
495
  }
492
- return best;
496
+ return standards.some((s) => documentEnforcesJurisdiction(s.document)) ? { ...best, jurisdictionRequired: true } : best;
493
497
  }
494
498
  function configValueValid(field, value) {
495
499
  switch (field.type) {
@@ -587,12 +591,17 @@ var REASON_BY_OPERAND = {
587
591
  "mm:counterparty": "COUNTERPARTY_NOT_ALLOWED"
588
592
  };
589
593
  var AMOUNT_OPERANDS = /* @__PURE__ */ new Set(["mm:payAmount", "mm:cumulativeSpend"]);
594
+ var JURISDICTION_OPERANDS = /* @__PURE__ */ new Set(["mm:jurisdiction", "jurisdiction"]);
590
595
  function reasonFor(constraint, req) {
591
596
  if (!constraint) return "CONSTRAINT_FAILED";
592
597
  const { leftOperand } = constraint;
593
598
  if (AMOUNT_OPERANDS.has(leftOperand) && !Object.prototype.hasOwnProperty.call(req.values, leftOperand)) {
594
599
  return "AMOUNT_NOT_DETERMINABLE";
595
600
  }
601
+ if (JURISDICTION_OPERANDS.has(leftOperand)) {
602
+ const v = req.values[leftOperand];
603
+ return v === void 0 || v === null || v === "" ? "JURISDICTION_REQUIRED" : "JURISDICTION_NOT_ALLOWED";
604
+ }
596
605
  return REASON_BY_OPERAND[leftOperand] ?? `CONSTRAINT_FAILED:${leftOperand}`;
597
606
  }
598
607
  function constraintSatisfied(c, req, strict) {
@@ -703,11 +712,14 @@ function evaluate(input) {
703
712
  }
704
713
 
705
714
  // src/policy-core/canonical.ts
715
+ var AUTH_MESSAGE_V2_TAG = "MAGP-AUTH-v2";
706
716
  function escapeField(v) {
707
717
  return v.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
708
718
  }
709
719
  function buildAuthMessage(f) {
710
- return [f.agentDid, f.action, f.amount, f.currency, f.merchant ?? "", f.resource ?? "", f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
720
+ const v1 = [f.agentDid, f.action, f.amount, f.currency, f.merchant ?? "", f.resource ?? "", f.nonce, f.issuedAt];
721
+ const fields = f.jurisdiction === void 0 || f.jurisdiction === null ? v1 : [...v1, AUTH_MESSAGE_V2_TAG, f.jurisdiction];
722
+ return fields.map((v) => escapeField(String(v))).join("|");
711
723
  }
712
724
  function buildLegacyAuthMessageV1(f) {
713
725
  return [f.agentDid, f.action, f.amount, f.currency, f.merchant ?? "", f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
@@ -768,8 +780,10 @@ export {
768
780
  ATOM_DEFAULT_REQUIRED_CONTEXT,
769
781
  ATOM_REGISTRY,
770
782
  ATOM_SPECS,
783
+ AUTH_MESSAGE_V2_TAG,
771
784
  CATALOGUED_ATOMS,
772
785
  CONTEXT_UNVERIFIABLE,
786
+ JURISDICTION_ATOM,
773
787
  MODES_BY_RANK,
774
788
  MODE_RANK,
775
789
  PROVENANCE_KEY,
@@ -789,6 +803,7 @@ export {
789
803
  buildRuleContext,
790
804
  canAuthorize,
791
805
  contextFieldProblem,
806
+ documentEnforcesJurisdiction,
792
807
  evaluate,
793
808
  evaluateBoundStandards,
794
809
  evaluateMandate,