@ouro.bot/cli 0.1.0-alpha.886 → 0.1.0-alpha.887

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.
Files changed (35) hide show
  1. package/changelog.json +21 -0
  2. package/deploy/unraid/README.txt +7 -5
  3. package/deploy/unraid/sanctuary-butler-upgrade.mjs +87 -30
  4. package/deploy/unraid/sanctuary-replay-gate.mjs +473 -74
  5. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1 -0
  6. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  7. package/deploy/unraid/sanctuary.xml +2 -1
  8. package/dist/a2a/client.js +16 -4
  9. package/dist/a2a/delegated-command-grants.js +93 -0
  10. package/dist/a2a/delegated-command.js +57 -11
  11. package/dist/a2a/escalation-grants.js +37 -75
  12. package/dist/a2a/identity.js +54 -0
  13. package/dist/a2a/operator-trust.js +382 -0
  14. package/dist/a2a/outbox-wire.js +2 -2
  15. package/dist/a2a/replay-grant-holders.js +33 -0
  16. package/dist/a2a/server.js +12 -7
  17. package/dist/a2a/trust-commands.js +203 -0
  18. package/dist/a2a/trusted-files.js +113 -26
  19. package/dist/heart/core.js +9 -5
  20. package/dist/heart/daemon/cli-exec.js +38 -37
  21. package/dist/heart/daemon/cli-parse.js +45 -5
  22. package/dist/heart/daemon/container-spec-auditor.js +3 -0
  23. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
  24. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +1 -1
  25. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +4 -47
  26. package/dist/heart/failure-reports.js +5 -5
  27. package/dist/mind/prompt.js +31 -1
  28. package/dist/repertoire/guardrails.js +89 -7
  29. package/dist/repertoire/house-sweep.js +4 -1
  30. package/dist/senses/a2a-entry.js +2 -1
  31. package/dist/senses/private-runtime.js +11 -4
  32. package/dist/senses/shared-turn.js +5 -1
  33. package/dist/senses/telegram.js +5 -3
  34. package/npm-shrinkwrap.json +2 -2
  35. package/package.json +1 -1
@@ -506,6 +506,7 @@ async function containerSnapshot(expectedImage) {
506
506
  { destination: "/run/ouro-events", source: PRODUCTION_EVENT_SPOOL_SOURCE, propagation: "rprivate", rw: false, type: "bind" },
507
507
  { destination: "/run/ouro-authority", source: "/run/ouro-authority", propagation: "rprivate", rw: false, type: "bind" },
508
508
  { destination: "/home/ouro/AgentBundles/sanctuary.ouro/psyche", source: `${PRODUCTION_BUNDLE_SOURCE}/psyche`, propagation: "rprivate", rw: false, type: "bind" },
509
+ { destination: "/etc/ouro/trust/sanctuary", source: "/mnt/user/appdata/ouro-butler/trust/sanctuary", propagation: "rprivate", rw: false, type: "bind" },
509
510
  ]
510
511
  const mountsExact = mounts.length === expectedMounts.length && expectedMounts.every((expected) => mounts.some((mount) => mount.destination === expected.destination && mount.source === expected.source && mount.propagation === expected.propagation && mount.rw === expected.rw && mount.type === expected.type))
511
512
  const securityExact = value.privileged === false && (value.capAdd === null || (Array.isArray(value.capAdd) && value.capAdd.length === 0))
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.886",
2
+ "runtimeVersion": "0.1.0-alpha.887",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.886</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.887</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -26,4 +26,5 @@
26
26
  <Config Name="Privileged event spool" Target="/run/ouro-events" Default="/boot/config/custom/ouro-events/spool" Mode="ro" Description="Root-owned detector envelopes, read-only inside the Butler" Type="Path" Display="always" Required="true" Mask="false">/boot/config/custom/ouro-events/spool</Config>
27
27
  <Config Name="Root authority socket" Target="/run/ouro-authority" Default="/run/ouro-authority" Mode="ro" Description="Root-owned gateway socket and public issuer pins only" Type="Path" Display="always" Required="true" Mask="false">/run/ouro-authority</Config>
28
28
  <Config Name="Psyche (read-only)" Target="/home/ouro/AgentBundles/sanctuary.ouro/psyche" Default="/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/psyche" Mode="ro" Description="The Butler's psyche files, read-only inside the Butler so they can be neither edited nor replaced" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/psyche</Config>
29
+ <Config Name="Operator trust (read-only)" Target="/etc/ouro/trust/sanctuary" Default="/mnt/user/appdata/ouro-butler/trust/sanctuary" Mode="ro" Description="Root-owned grants the Butler honours (delegated commands, escalation). Read-only inside the Butler; only the host operator can change it" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/trust/sanctuary</Config>
29
30
  </Container>
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.A2ARpcError = void 0;
3
4
  exports.cardDid = cardDid;
4
5
  exports.endpointForCard = endpointForCard;
5
6
  exports.fetchA2AAgentCard = fetchA2AAgentCard;
@@ -50,6 +51,17 @@ async function fetchA2AAgentCard(cardUrl, fetchImpl = fetch) {
50
51
  });
51
52
  return parsed;
52
53
  }
54
+ /** A JSON-RPC error answer from the peer. `data` carries the peer's structured detail (for a refused delegated command: reason, nothingRan, retry, next). */
55
+ class A2ARpcError extends Error {
56
+ code;
57
+ data;
58
+ constructor(error) {
59
+ super(`A2A error ${error.code}: ${error.message}`);
60
+ this.code = error.code;
61
+ this.data = error.data;
62
+ }
63
+ }
64
+ exports.A2ARpcError = A2ARpcError;
53
65
  async function postJsonRpc(endpointUrl, request, fetchImpl, protocolVersion = "1.0") {
54
66
  const response = await fetchImpl(endpointUrl, {
55
67
  method: "POST",
@@ -112,7 +124,7 @@ async function sendA2AMessage(input) {
112
124
  }, fetchImpl);
113
125
  }
114
126
  if ("error" in rpc) {
115
- throw new Error(`A2A error ${rpc.error.code}: ${rpc.error.message}`);
127
+ throw new A2ARpcError(rpc.error);
116
128
  }
117
129
  const result = rpc.result;
118
130
  const task = "task" in result && result.task ? result.task : result;
@@ -151,7 +163,7 @@ async function postA2AMessageEnvelope(input) {
151
163
  rpc = await postJsonRpc(input.endpointUrl, { ...request, method: "SendMessage" }, fetchImpl);
152
164
  }
153
165
  if ("error" in rpc) {
154
- throw new Error(`A2A error ${rpc.error.code}: ${rpc.error.message}`);
166
+ throw new A2ARpcError(rpc.error);
155
167
  }
156
168
  (0, runtime_1.emitNervesEvent)({
157
169
  component: "channels",
@@ -184,7 +196,7 @@ async function getA2ATask(input) {
184
196
  rpc = await postJsonRpc(input.endpointUrl, { ...request, method: "GetTask" }, fetchImpl);
185
197
  }
186
198
  if ("error" in rpc) {
187
- throw new Error(`A2A error ${rpc.error.code}: ${rpc.error.message}`);
199
+ throw new A2ARpcError(rpc.error);
188
200
  }
189
201
  return rpc.result;
190
202
  }
@@ -225,7 +237,7 @@ async function sendSealedA2AChat(input) {
225
237
  (0, runtime_1.emitNervesEvent)({ component: "channels", event: "channel.a2a_chat_send_start", message: "sending sealed A2A chat message", meta: { endpointUrl, peerDid } });
226
238
  const rpc = await postJsonRpc(endpointUrl, request, fetchImpl);
227
239
  if ("error" in rpc)
228
- throw new Error(`A2A error ${rpc.error.code}: ${rpc.error.message}`);
240
+ throw new A2ARpcError(rpc.error);
229
241
  const task = rpc.result;
230
242
  const replyMessage = task?.status?.message;
231
243
  if (!replyMessage)
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DELEGATED_COMMAND_GRANTS_FILE = void 0;
4
+ exports.delegatedCommandGrantsPath = delegatedCommandGrantsPath;
5
+ exports.viewDelegatedCommandGrants = viewDelegatedCommandGrants;
6
+ exports.readDelegatedCommandGrants = readDelegatedCommandGrants;
7
+ exports.checkDelegatedCommandGrant = checkDelegatedCommandGrant;
8
+ exports.setDelegatedCommandGrant = setDelegatedCommandGrant;
9
+ const runtime_1 = require("../nerves/runtime");
10
+ const operator_trust_1 = require("./operator-trust");
11
+ const replay_grant_holders_1 = require("./replay-grant-holders");
12
+ const replay_harness_1 = require("./replay-harness");
13
+ exports.DELEGATED_COMMAND_GRANTS_FILE = "delegated-command-grants.json";
14
+ function delegatedCommandGrantsPath(agentRoot) {
15
+ return (0, operator_trust_1.operatorTrustFile)(agentRoot, exports.DELEGATED_COMMAND_GRANTS_FILE);
16
+ }
17
+ function validGrant(value) {
18
+ if (!value || typeof value !== "object" || Array.isArray(value))
19
+ return false;
20
+ const grant = value;
21
+ const keys = Object.keys(grant).sort().join(",");
22
+ return (keys === "did,grantedAt,scope,source" || keys === "did,expiresAt,grantedAt,scope,source")
23
+ && grant.scope === "principal_commands"
24
+ && typeof grant.did === "string" && grant.did.startsWith("did:") && grant.did.length <= 512
25
+ && typeof grant.grantedAt === "string" && !Number.isNaN(Date.parse(grant.grantedAt))
26
+ && typeof grant.source === "string" && grant.source.trim().length > 0
27
+ && (grant.expiresAt === undefined || typeof grant.expiresAt === "string");
28
+ }
29
+ /** The file's trust state, every well-formed grant in it keyed by friend id, and the friend ids whose entries were dropped. */
30
+ function viewDelegatedCommandGrants(agentRoot) {
31
+ return (0, operator_trust_1.readGrantFile)(agentRoot, exports.DELEGATED_COMMAND_GRANTS_FILE, validGrant);
32
+ }
33
+ function readDelegatedCommandGrants(agentRoot) {
34
+ return viewDelegatedCommandGrants(agentRoot).grants;
35
+ }
36
+ /**
37
+ * The one answer to "may this friend relay the principal's commands right now?". Admission, the follow-up path and
38
+ * `list` all ask it, so they cannot disagree. `signerDid` is the DID the message was verified as: it must equal the
39
+ * DID the operator pinned. Order: file trusted, grant exists, DID matches, not expired (an expiry that is not a finite
40
+ * time counts as expired, and so does a replay identity whose root-owned window is closed), then family and active.
41
+ */
42
+ function checkDelegatedCommandGrant(agentRoot, friend, signerDid, now = Date.now()) {
43
+ const view = viewDelegatedCommandGrants(agentRoot);
44
+ if (view.state === "untrusted")
45
+ return { ok: false, reason: "grants_untrusted" };
46
+ const grant = view.grants[friend.id];
47
+ if (!grant)
48
+ return { ok: false, reason: "no_grant" };
49
+ if (grant.did !== signerDid)
50
+ return { ok: false, reason: "grant_did_mismatch" };
51
+ if (grant.expiresAt !== undefined) {
52
+ const expiresAt = Date.parse(grant.expiresAt);
53
+ if (!Number.isFinite(expiresAt) || expiresAt <= now)
54
+ return { ok: false, reason: "grant_expired" };
55
+ }
56
+ // A grant for a friend on the root-owned replay list must be time-bounded by the trusted file itself. The bundle's replay registry is the agent's to move aside, so it only ever adds the window check below.
57
+ if (grant.expiresAt === undefined && (0, replay_grant_holders_1.isReplayGrantHolder)(agentRoot, friend.id))
58
+ return { ok: false, reason: "grant_expired" };
59
+ // A replay identity is a test peer of the host's replay gate: its grant means something only while the root-owned window is open.
60
+ if ((0, replay_harness_1.isReplayIdentity)(agentRoot, friend.id) && !(0, replay_harness_1.isReplayWindowOpen)(agentRoot, friend.id, now))
61
+ return { ok: false, reason: "grant_expired" };
62
+ if (friend.trustLevel !== "family" || friend.admissionState !== "active")
63
+ return { ok: false, reason: "not_family" };
64
+ return { ok: true };
65
+ }
66
+ /** Writes or removes one grant. The previous file is kept beside it as a timestamped backup before the change. */
67
+ function setDelegatedCommandGrant(agentRoot, friendId, change, now = new Date()) {
68
+ return (0, operator_trust_1.withTrustedWriteLock)(agentRoot, () => setDelegatedCommandGrantLocked(agentRoot, friendId, change, now));
69
+ }
70
+ function setDelegatedCommandGrantLocked(agentRoot, friendId, change, now) {
71
+ const current = readDelegatedCommandGrants(agentRoot);
72
+ const had = current[friendId];
73
+ const next = { ...current };
74
+ if (change.grant) {
75
+ const entry = { scope: "principal_commands", did: change.did, grantedAt: now.toISOString(), source: change.source, ...(change.expiresAt ? { expiresAt: change.expiresAt } : {}) };
76
+ if (had && had.did === entry.did && had.source === entry.source && had.expiresAt === entry.expiresAt)
77
+ return { changed: false, backup: null };
78
+ next[friendId] = entry;
79
+ }
80
+ else {
81
+ if (had === undefined)
82
+ return { changed: false, backup: null };
83
+ delete next[friendId];
84
+ }
85
+ const { backup } = (0, operator_trust_1.writeTrustedFile)(agentRoot, exports.DELEGATED_COMMAND_GRANTS_FILE, `${JSON.stringify({ schemaVersion: 1, grants: next }, null, 2)}\n`, now);
86
+ (0, runtime_1.emitNervesEvent)({
87
+ component: "senses",
88
+ event: "senses.a2a_delegated_command_grant_changed",
89
+ message: change.grant ? "granted delegated commands to an A2A peer" : "revoked an A2A peer's delegated-command grant",
90
+ meta: { friendId, granted: change.grant },
91
+ });
92
+ return { changed: true, backup };
93
+ }
@@ -35,12 +35,15 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DELEGATED_BANNER = exports.A2A_PRINCIPAL_PROFILE_ID = void 0;
37
37
  exports.delegatedCommandWasNoticed = delegatedCommandWasNoticed;
38
+ exports.delegationRefusalGuidance = delegationRefusalGuidance;
39
+ exports.shieldDelegatedBanner = shieldDelegatedBanner;
38
40
  exports.delegatedCommandNotice = delegatedCommandNotice;
39
41
  exports.admitDelegatedCommand = admitDelegatedCommand;
40
42
  const crypto_1 = require("crypto");
41
43
  const fs = __importStar(require("fs"));
42
44
  const path = __importStar(require("path"));
43
45
  const runtime_1 = require("../nerves/runtime");
46
+ const delegated_command_grants_1 = require("./delegated-command-grants");
44
47
  const replay_harness_1 = require("./replay-harness");
45
48
  const relationship_authorization_1 = require("../repertoire/relationship-authorization");
46
49
  const NOTICE_EXCERPT_CHARS = 200;
@@ -67,16 +70,60 @@ function delegatedCommandWasNoticed(agentRoot, commandId, principalFriendId, del
67
70
  const artifact = JSON.parse(raw);
68
71
  return artifact.idempotencyKey === idempotencyKey && artifact.authorClass === "butler" && artifact.target?.friendId === principalFriendId;
69
72
  }
70
- /** Matches the banner only the server may put in front of an admitted command. */
71
- exports.DELEGATED_BANNER = /^\s*\[\s*delegated command/iu;
73
+ /**
74
+ * What a refused sender is told. Every refusal says that nothing ran and names the next step; the reason code stays
75
+ * first on the wire (`delegated command refused: <reason>`) because the replay gate and older CLIs match it.
76
+ */
77
+ function delegationRefusalGuidance(reason, hints) {
78
+ switch (reason) {
79
+ case "not_enabled":
80
+ return { message: "this agent does not accept delegated commands", next: "Send the message without --delegated, or ask the agent's operator to enable delegated commands.", retry: false };
81
+ case "no_grant":
82
+ return {
83
+ message: hints.legacyRecordGrant
84
+ ? "the operator has not granted you delegated commands; a grant on your friend record is not honoured, because the agent could have written it"
85
+ : "the operator has not granted you delegated commands",
86
+ next: `Ask the operator to run, as root on the agent's host: ouro a2a delegated-commands grant --friend <your friend id> --did <your DID> (your DID is in: ouro a2a identity --json).${hints.legacyRecordGrant ? " Grants live in the operator trust directory now, not on the friend record." : ""}`,
87
+ retry: false,
88
+ };
89
+ case "grants_untrusted":
90
+ return { message: "the agent cannot trust its grant file, so it honours no delegated-command grants", next: "Ask the operator to run: ouro a2a delegated-commands list, and fix the owner or mode it reports.", retry: false };
91
+ case "grant_did_mismatch":
92
+ return { message: "this message was signed by a different key than the one the operator granted", next: "Send from the host whose DID the operator pinned, or ask the operator to re-grant with your current DID: ouro a2a delegated-commands grant --friend <your friend id> --did <your DID>.", retry: false };
93
+ case "grant_expired":
94
+ return { message: "your delegated-command grant has expired or is not in force", next: "Ask the operator to re-grant: ouro a2a delegated-commands grant --friend <your friend id> --did <your DID> [--expires <ISO date>].", retry: false };
95
+ case "not_family":
96
+ return { message: "you are not active family on this agent's records, so the grant is suspended", next: "Ask the operator to restore your trust and admission, then try again.", retry: false };
97
+ case "principal_unresolved":
98
+ return { message: "the agent cannot tell whose command this is (its owner record is missing or ambiguous)", next: "Ask the operator to check the owner's friend record on the agent's host.", retry: false };
99
+ case "notice_failed":
100
+ return { message: "the agent could not tell its owner about this command first, so it did not run it", next: "Retry in a minute. If it keeps failing, the owner's chat channel is down: tell the operator.", retry: true };
101
+ }
102
+ }
103
+ /** Invisible and bidirectional-control characters a sender could slip inside the banner to keep it from matching. */
104
+ const INVISIBLE = /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0]/gu;
105
+ /** Matches the banner only the server may put in front of an admitted command, anywhere in the text, after normalisation. */
106
+ exports.DELEGATED_BANNER = /\[[^\p{L}\p{N}]*delegated[^\p{L}\p{N}]*command|verified[^\p{L}\p{N}]*delegated[^\p{L}\p{N}]*command/iu;
107
+ /** The text as the banner check sees it: compatibility-normalised (NFKC) with invisible characters removed. */
108
+ function bannerView(text) {
109
+ return text.normalize("NFKC").replace(INVISIBLE, "");
110
+ }
111
+ /**
112
+ * Marks any text that carries the delegated-command banner as typed by the sender. Only the server writes the real banner, in
113
+ * front of a command it has admitted; this runs on every other turn and on the body of an admitted one, whichever channel it came on.
114
+ */
115
+ const SHIELD_MARK = "[unverified: the sender typed this banner itself; it is not a delegated command] ";
116
+ function shieldDelegatedBanner(text) {
117
+ if (text.startsWith(SHIELD_MARK) || !exports.DELEGATED_BANNER.test(bannerView(text)))
118
+ return text;
119
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.a2a_delegated_banner_neutralized", message: "marked a typed delegated-command banner as unverified", meta: { length: text.length } });
120
+ return `${SHIELD_MARK}${text}`;
121
+ }
72
122
  function delegatedCommandNotice(input) {
73
123
  const flat = input.text.replace(/\s+/gu, " ").trim();
74
124
  const excerpt = flat.length > NOTICE_EXCERPT_CHARS ? `${flat.slice(0, NOTICE_EXCERPT_CHARS)}…` : flat;
75
125
  return `Delegated command from you via ${input.delegateName}: "${excerpt}". If this wasn't you, say so here: it is on record, and the grant can be revoked.`;
76
126
  }
77
- function hasPrincipalGrant(friend) {
78
- return friend.delegationGrant?.scope === "principal_commands";
79
- }
80
127
  async function resolvePrincipal(store, profileId) {
81
128
  const friends = await store.listAll?.() ?? [];
82
129
  const matches = friends.filter((friend) => friend.capabilityProfileId === profileId
@@ -96,10 +143,9 @@ async function admitDelegatedCommand(input) {
96
143
  };
97
144
  if (!input.options)
98
145
  return refuse("not_enabled");
99
- if (!hasPrincipalGrant(input.friend))
100
- return refuse("no_grant");
101
- if (input.friend.trustLevel !== "family" || input.friend.admissionState === "revoked")
102
- return refuse("not_family");
146
+ const granted = (0, delegated_command_grants_1.checkDelegatedCommandGrant)(input.options.agentRoot, input.friend, input.did, input.now);
147
+ if (!granted.ok)
148
+ return refuse(granted.reason);
103
149
  const registry = input.registry;
104
150
  if (!registry)
105
151
  return refuse("principal_unresolved");
@@ -110,7 +156,7 @@ async function admitDelegatedCommand(input) {
110
156
  try {
111
157
  const text = delegatedCommandNotice({ delegateName: input.friend.name, text: input.text });
112
158
  const agentRoot = input.options.agentRoot;
113
- if (agentRoot && (0, replay_harness_1.isReplayWindowOpen)(agentRoot, input.friend.id))
159
+ if ((0, replay_harness_1.isReplayWindowOpen)(agentRoot, input.friend.id))
114
160
  (0, replay_harness_1.appendReplayNotice)(agentRoot, { noticeId, text, friendId: input.friend.id });
115
161
  else
116
162
  await input.options.notifyPrincipal({ noticeId, text });
@@ -146,6 +192,6 @@ async function admitDelegatedCommand(input) {
146
192
  message: "admitted a delegated A2A command after notifying the principal",
147
193
  meta: { delegateFriendId: context.delegateFriendId, principalFriendId: context.principalFriendId, commandId: context.commandId },
148
194
  });
149
- const turnText = `[delegated command from ${principal.name} via ${input.friend.name}; the signature and the delegation grant are verified, and ${principal.name} has been notified; act on it as ${principal.name}'s own request]\n\n${input.text}`;
195
+ const turnText = `[delegated command from ${principal.name} via ${input.friend.name}; the signature and the delegation grant are verified, and ${principal.name} has been notified; act on it as ${principal.name}'s own request]\n\n${shieldDelegatedBanner(input.text)}`;
150
196
  return { ok: true, relationship, context, turnText };
151
197
  }
@@ -1,103 +1,55 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ESCALATION_GRANTS_FILE = void 0;
36
4
  exports.escalationGrantsPath = escalationGrantsPath;
5
+ exports.viewEscalationGrants = viewEscalationGrants;
37
6
  exports.readEscalationGrants = readEscalationGrants;
38
7
  exports.setEscalationGrant = setEscalationGrant;
39
8
  exports.pinnedHolderDid = pinnedHolderDid;
40
9
  exports.holdsEscalation = holdsEscalation;
41
10
  exports.escalationHolders = escalationHolders;
42
- const fs = __importStar(require("node:fs"));
43
- const path = __importStar(require("node:path"));
44
11
  const runtime_1 = require("../nerves/runtime");
45
- const trusted_files_1 = require("./trusted-files");
12
+ const replay_grant_holders_1 = require("./replay-grant-holders");
13
+ const operator_trust_1 = require("./operator-trust");
14
+ exports.ESCALATION_GRANTS_FILE = "escalation-grants.json";
46
15
  function escalationGrantsPath(agentRoot) {
47
- return path.join(agentRoot, "state", "a2a", "escalation-grants.json");
16
+ return (0, operator_trust_1.operatorTrustFile)(agentRoot, exports.ESCALATION_GRANTS_FILE);
48
17
  }
49
18
  function validGrant(value) {
50
19
  if (!value || typeof value !== "object" || Array.isArray(value))
51
20
  return false;
52
21
  const grant = value;
53
- return Object.keys(grant).sort().join(",") === "did,grantedAt,scope,source"
22
+ const keys = Object.keys(grant).sort().join(",");
23
+ return (keys === "did,grantedAt,scope,source" || keys === "did,expiresAt,grantedAt,scope,source")
24
+ && (grant.expiresAt === undefined || typeof grant.expiresAt === "string")
54
25
  && typeof grant.did === "string" && grant.did.startsWith("did:") && grant.did.length <= 512
55
26
  && grant.scope === "escalation"
56
27
  && typeof grant.grantedAt === "string" && !Number.isNaN(Date.parse(grant.grantedAt))
57
28
  && typeof grant.source === "string" && grant.source.trim().length > 0;
58
29
  }
30
+ /** The file's trust state and every well-formed grant in it, keyed by friend id. */
31
+ function viewEscalationGrants(agentRoot) {
32
+ return (0, operator_trust_1.readGrantFile)(agentRoot, exports.ESCALATION_GRANTS_FILE, validGrant);
33
+ }
59
34
  /** Every well-formed grant in the file, keyed by friend id. Anything else in the file is ignored. */
60
35
  function readEscalationGrants(agentRoot) {
61
- const file = escalationGrantsPath(agentRoot);
62
- if (!(0, trusted_files_1.isTrustedDirectory)(path.dirname(file)))
63
- return {};
64
- const parsed = (0, trusted_files_1.readTrustedJson)(file);
65
- const grants = parsed?.grants;
66
- if (!grants || typeof grants !== "object" || Array.isArray(grants))
67
- return {};
68
- return Object.fromEntries(Object.entries(grants).filter((entry) => validGrant(entry[1])));
69
- }
70
- /** Run as root, the operator's grant leaves the file and its directory root-owned and closed to everyone else, even if an earlier recursive chown had handed them to the resident. */
71
- function ownAsRoot(target, mode) {
72
- fs.chownSync(target, 0, 0);
73
- fs.chmodSync(target, mode);
36
+ return viewEscalationGrants(agentRoot).grants;
74
37
  }
75
38
  /** Writes or removes one grant. The previous file is kept beside it as a timestamped backup before the change. */
76
39
  function setEscalationGrant(agentRoot, friendId, change, now = new Date()) {
77
- const file = escalationGrantsPath(agentRoot);
78
- // Root repairs the directory first; a grants file the resident owns stays untrusted and is replaced, never blessed.
79
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o755 });
80
- if (process.geteuid?.() === 0)
81
- ownAsRoot(path.dirname(file), 0o755);
40
+ return (0, operator_trust_1.withTrustedWriteLock)(agentRoot, () => setEscalationGrantLocked(agentRoot, friendId, change, now));
41
+ }
42
+ function setEscalationGrantLocked(agentRoot, friendId, change, now) {
82
43
  const current = readEscalationGrants(agentRoot);
83
44
  const had = current[friendId];
84
- if (change.grant ? had?.did === change.did : had === undefined)
45
+ if (change.grant ? had?.did === change.did && had.expiresAt === change.expiresAt : had === undefined)
85
46
  return { changed: false, backup: null };
86
- let backup = null;
87
- if (fs.existsSync(file)) {
88
- backup = `${file}.bak-${now.toISOString().replace(/[:.]/gu, "-")}`;
89
- fs.copyFileSync(file, backup);
90
- }
91
47
  const next = { ...current };
92
48
  if (change.grant)
93
- next[friendId] = { scope: "escalation", grantedAt: now.toISOString(), source: change.source, did: change.did };
49
+ next[friendId] = { scope: "escalation", grantedAt: now.toISOString(), source: change.source, did: change.did, ...(change.expiresAt ? { expiresAt: change.expiresAt } : {}) };
94
50
  else
95
51
  delete next[friendId];
96
- const tmp = `${file}.${process.pid}.tmp`;
97
- fs.writeFileSync(tmp, `${JSON.stringify({ schemaVersion: 1, grants: next }, null, 2)}\n`, { mode: 0o644 });
98
- fs.renameSync(tmp, file);
99
- if (process.geteuid?.() === 0)
100
- ownAsRoot(file, 0o644);
52
+ const { backup } = (0, operator_trust_1.writeTrustedFile)(agentRoot, exports.ESCALATION_GRANTS_FILE, `${JSON.stringify({ schemaVersion: 1, grants: next }, null, 2)}\n`, now);
101
53
  (0, runtime_1.emitNervesEvent)({
102
54
  component: "senses",
103
55
  event: "senses.a2a_escalation_grant_changed",
@@ -106,17 +58,27 @@ function setEscalationGrant(agentRoot, friendId, change, now = new Date()) {
106
58
  });
107
59
  return { changed: true, backup };
108
60
  }
109
- /** The DID the operator pinned for this holder, or null when it holds no (complete) grant. */
110
- function pinnedHolderDid(agentRoot, friendId) {
111
- return readEscalationGrants(agentRoot)[friendId]?.did ?? null;
61
+ /** A grant is in force when its trusted expiry has not passed; a grant with no expiry for a friend on the root-owned replay list is never in force. */
62
+ function inForce(agentRoot, friendId, grant, now) {
63
+ if (!grant)
64
+ return false;
65
+ if (grant.expiresAt === undefined)
66
+ return !(0, replay_grant_holders_1.isReplayGrantHolder)(agentRoot, friendId);
67
+ const expiresAt = Date.parse(grant.expiresAt);
68
+ return Number.isFinite(expiresAt) && expiresAt > now;
69
+ }
70
+ /** The DID the operator pinned for this holder, or null when it holds no (complete, unexpired) grant. */
71
+ function pinnedHolderDid(agentRoot, friendId, now = Date.now()) {
72
+ const grant = readEscalationGrants(agentRoot)[friendId];
73
+ return inForce(agentRoot, friendId, grant, now) ? grant.did : null;
112
74
  }
113
75
  /** True only for an active family friend that the operator listed in the grants file, with a pinned DID. */
114
- function holdsEscalation(agentRoot, friend) {
115
- return readEscalationGrants(agentRoot)[friend.id] !== undefined
76
+ function holdsEscalation(agentRoot, friend, now = Date.now()) {
77
+ return inForce(agentRoot, friend.id, readEscalationGrants(agentRoot)[friend.id], now)
116
78
  && friend.trustLevel === "family"
117
79
  && friend.admissionState === "active";
118
80
  }
119
- async function escalationHolders(agentRoot, store) {
81
+ async function escalationHolders(agentRoot, store, now = Date.now()) {
120
82
  const friends = await store.listAll?.() ?? [];
121
- return friends.filter((friend) => holdsEscalation(agentRoot, friend));
83
+ return friends.filter((friend) => holdsEscalation(agentRoot, friend, now));
122
84
  }
@@ -33,9 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PUBLIC_A2A_IDENTITY_FILE = void 0;
36
37
  exports.readStoredA2ASeed = readStoredA2ASeed;
37
38
  exports.loadOrMintA2AIdentity = loadOrMintA2AIdentity;
38
39
  exports.loadSelfA2AIdentity = loadSelfA2AIdentity;
40
+ exports.publishOwnA2ADid = publishOwnA2ADid;
41
+ exports.readPublishedA2ADid = readPublishedA2ADid;
42
+ exports.readOwnA2ADid = readOwnA2ADid;
39
43
  exports.loadOrMintA2AIdentityFile = loadOrMintA2AIdentityFile;
40
44
  const crypto = __importStar(require("node:crypto"));
41
45
  const fs = __importStar(require("node:fs"));
@@ -171,6 +175,56 @@ async function loadSelfA2AIdentity(input) {
171
175
  },
172
176
  });
173
177
  }
178
+ /** Where the agent publishes its own DID inside its bundle: a public value (a DID is a public key), never the seed. */
179
+ exports.PUBLIC_A2A_IDENTITY_FILE = path.join("a2a", "public-identity.json");
180
+ /**
181
+ * Publish this agent's own DID into its bundle so a one-off operator command (which has no vault access) can compare against it
182
+ * through the read-only bundle mount. Best effort: a read-only bundle just means the fallback stays unavailable.
183
+ */
184
+ function publishOwnA2ADid(agentRoot, did) {
185
+ try {
186
+ const file = path.join(agentRoot, exports.PUBLIC_A2A_IDENTITY_FILE);
187
+ if (readPublishedA2ADid(agentRoot) === did)
188
+ return;
189
+ fs.mkdirSync(path.dirname(file), { recursive: true });
190
+ fs.writeFileSync(file, `${JSON.stringify({ did }, null, 2)}\n`);
191
+ }
192
+ catch {
193
+ // The bundle may be mounted read-only; the DID then simply is not published.
194
+ }
195
+ }
196
+ /**
197
+ * The DID published in the bundle's public identity file, or null when it is absent or malformed. Read-only and ADVISORY: the agent can
198
+ * write its own bundle, so this value is never proof of which key is the agent's. Operator checks may report it, never rely on it alone.
199
+ */
200
+ function readPublishedA2ADid(agentRoot) {
201
+ try {
202
+ const parsed = JSON.parse(fs.readFileSync(path.join(agentRoot, exports.PUBLIC_A2A_IDENTITY_FILE), "utf8"));
203
+ return typeof parsed.did === "string" && parsed.did.startsWith("did:key:") ? parsed.did : null;
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ /**
210
+ * This agent's own A2A DID if its seed is already in this process's cached machine config, else null. Strictly read-only: it
211
+ * never mints a seed, never creates the machine identity file and never refreshes the vault, because an operator command only
212
+ * wants to compare, not to create or fetch anything. A one-off root command has no cached config, so it gets null and the caller
213
+ * falls back to the published DID (advisory only, see `readPublishedA2ADid`) and to comparing the DID against the friend records.
214
+ */
215
+ async function readOwnA2ADid(agentName) {
216
+ try {
217
+ const cached = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agentName);
218
+ if (!cached.ok || !readStoredA2ASeed(cached.config))
219
+ return null;
220
+ /* v8 ignore next -- the seed is already stored here, so the load never reaches upsert; it exists only to make minting impossible @preserve */
221
+ const identity = await loadOrMintA2AIdentity({ agentName, sodium: await (0, a2a_client_1.ready)(), config: cached.config, upsert: async () => { throw new Error("readOwnA2ADid never mints"); } });
222
+ return identity.did;
223
+ }
224
+ catch {
225
+ return null;
226
+ }
227
+ }
174
228
  /**
175
229
  * A file-backed A2A identity for a client that is not an ouro agent (for example a
176
230
  * coding harness talking to an agent over A2A). The file holds the Ed25519 seed, so