@ouro.bot/cli 0.1.0-alpha.815 → 0.1.0-alpha.817

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 (63) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +18 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +305 -365
  5. package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
  8. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  9. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  10. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
  11. package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  13. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  14. package/deploy/unraid/sanctuary.xml +2 -1
  15. package/dist/heart/approval-store.js +11 -1
  16. package/dist/heart/core.js +2 -1
  17. package/dist/heart/daemon/container-spec-auditor-main.js +9 -8
  18. package/dist/heart/daemon/container-spec-auditor.js +17 -24
  19. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  20. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  21. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  22. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  23. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  24. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  25. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  26. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  27. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  28. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  29. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  30. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  31. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  33. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  36. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  37. package/dist/heart/daemon/sense-manager.js +20 -10
  38. package/dist/heart/external-events/router.js +31 -15
  39. package/dist/heart/steward-policy.js +374 -58
  40. package/dist/heart/tool-approval.js +8 -1
  41. package/dist/repertoire/relationship-authorization.js +128 -0
  42. package/dist/repertoire/tools-base.js +9 -13
  43. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  44. package/dist/repertoire/tools-steward-policy.js +34 -10
  45. package/dist/repertoire/tools-unraid.js +79 -32
  46. package/dist/repertoire/tools.js +37 -25
  47. package/dist/repertoire/unraid-restart.js +179 -52
  48. package/dist/senses/private-runtime.js +27 -12
  49. package/dist/senses/root-host-approval-port.js +345 -0
  50. package/dist/senses/root-host-approval-runtime.js +393 -0
  51. package/dist/senses/sanctuary-authority-resident.js +102 -0
  52. package/dist/senses/sanctuary-health-runner.js +0 -1
  53. package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
  54. package/dist/senses/sanctuary-runtime.js +2 -0
  55. package/dist/senses/telegram-admission.js +7 -0
  56. package/dist/senses/telegram-approval-runtime.js +130 -12
  57. package/dist/senses/telegram-attachments.js +5 -1
  58. package/dist/senses/telegram-authority-transport.js +231 -0
  59. package/dist/senses/telegram-client.js +31 -8
  60. package/dist/senses/telegram-entry.js +1 -1
  61. package/dist/senses/telegram.js +174 -28
  62. package/npm-shrinkwrap.json +2 -2
  63. package/package.json +1 -1
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.routineActionRequester = routineActionRequester;
37
+ exports.authorizeRestartApprovalRequester = authorizeRestartApprovalRequester;
38
+ exports.authorizeRoutineActionRequester = authorizeRoutineActionRequester;
36
39
  exports.renderRelationshipPreferences = renderRelationshipPreferences;
37
40
  exports.withRelationshipPreference = withRelationshipPreference;
38
41
  exports.loadRelationshipCapabilityRegistry = loadRelationshipCapabilityRegistry;
@@ -43,7 +46,132 @@ exports.authorizeRelationshipAccess = authorizeRelationshipAccess;
43
46
  const fs = __importStar(require("node:fs"));
44
47
  const path = __importStar(require("node:path"));
45
48
  const node_crypto_1 = require("node:crypto");
49
+ const node_util_1 = require("node:util");
50
+ const router_1 = require("../heart/external-events/router");
46
51
  const runtime_1 = require("../nerves/runtime");
52
+ function routineActionRequester(ctx, eventTarget) {
53
+ const relationship = ctx?.relationshipAuthorization;
54
+ if (!relationship)
55
+ return null;
56
+ if (ctx?.currentExternalEvent) {
57
+ if (!eventTarget || relationship.profileId !== "sanctuary-event")
58
+ return null;
59
+ const events = [ctx.currentExternalEvent, ...(ctx.currentExternalEvent.relatedEvents ?? [])].filter((event) => event.schemaVersion === 1 && event.source === "sanctuary-health" && event.eventId === `container:${eventTarget.target.id}:availability`);
60
+ if (events.length !== 1)
61
+ return null;
62
+ const event = events[0];
63
+ return {
64
+ kind: "owner_event", friendId: eventTarget.friendId, profileId: relationship.profileId,
65
+ event: { schemaVersion: 1, recordPath: event.recordPath, agent: event.agent, source: event.source, eventId: event.eventId, generation: event.generation, observationRevision: event.observationRevision, claimOwner: event.claimOwner },
66
+ target: { ...eventTarget.target },
67
+ };
68
+ }
69
+ const actor = relationship.actor;
70
+ const session = ctx?.currentSession;
71
+ if (!actor || !session || session.channel !== "telegram" || session.friendId !== actor.friendId
72
+ || [actor.friendId, actor.sessionEventId, relationship.requestId, session.key].some((value) => typeof value !== "string" || !value.trim()))
73
+ return null;
74
+ const kind = actor.trustLevel === "family" && relationship.profileId === "sanctuary-owner" ? "owner"
75
+ : actor.trustLevel === "friend" && relationship.profileId === "sanctuary-household" ? "household_request" : null;
76
+ if (!kind)
77
+ return null;
78
+ return { kind, friendId: actor.friendId, profileId: relationship.profileId, requestId: relationship.requestId, sessionEventId: actor.sessionEventId, origin: { friendId: session.friendId, channel: session.channel, key: session.key } };
79
+ }
80
+ function isRecord(value) {
81
+ return value !== null && typeof value === "object" && !Array.isArray(value);
82
+ }
83
+ function authorizeRestartApprovalRequester(ctx, args, binding) {
84
+ return authorizeRoutineActionRequester(ctx, args, {
85
+ requester: {
86
+ kind: "owner", friendId: binding.friendId, profileId: "sanctuary-owner", requestId: binding.requestId,
87
+ sessionEventId: binding.sessionEventId, origin: { friendId: binding.friendId, channel: "telegram", key: binding.sessionKey },
88
+ },
89
+ profileVersion: binding.profileVersion,
90
+ });
91
+ }
92
+ async function authorizeRoutineActionRequester(ctx, args, expected) {
93
+ if (!isRecord(args) || Object.keys(args).length !== 1 || typeof args.container !== "string"
94
+ || !args.container || args.container !== args.container.trim() || Buffer.byteLength(args.container) > 128 || args.container.includes("\uFFFD")
95
+ || !ctx?.agentRoot || !ctx.relationshipAuthorization)
96
+ return { allowed: false, reason: "routine action requires one exact target and current relationship" };
97
+ const target = args.container;
98
+ const agentRoot = ctx.agentRoot;
99
+ const relationship = ctx.relationshipAuthorization;
100
+ const sanctuary = ctx.sanctuary;
101
+ const initialRequester = routineActionRequester(ctx);
102
+ const event = ctx.currentExternalEvent && structuredClone(ctx.currentExternalEvent);
103
+ if (!initialRequester && (!event || event.source !== "sanctuary-health" || relationship.profileId !== "sanctuary-event"))
104
+ return { allowed: false, reason: "routine action requires a current human request or health event" };
105
+ const unchanged = () => ctx.agentRoot === agentRoot && ctx.relationshipAuthorization === relationship && ctx.sanctuary === sanctuary
106
+ && args.container === target && Object.keys(args).length === 1
107
+ && (0, node_util_1.isDeepStrictEqual)(initialRequester, routineActionRequester(ctx)) && (0, node_util_1.isDeepStrictEqual)(event, ctx.currentExternalEvent);
108
+ let authorization;
109
+ try {
110
+ authorization = await relationship.authorizeTool("unraid_restart_container", { container: target });
111
+ }
112
+ catch {
113
+ return { allowed: false, reason: "routine relationship authorization is unavailable" };
114
+ }
115
+ if (!authorization?.allowed)
116
+ return { allowed: false, reason: authorization?.allowed === false ? authorization.reason : "routine relationship authorization is unavailable" };
117
+ if (typeof authorization.profileVersion !== "number" || !Number.isSafeInteger(authorization.profileVersion) || authorization.profileVersion < 1
118
+ || typeof authorization.receiptId !== "string" || !authorization.receiptId.trim())
119
+ return { allowed: false, reason: "routine relationship authorization is not versioned" };
120
+ if (expected && authorization.profileVersion !== expected.profileVersion)
121
+ return { allowed: false, reason: "routine relationship profile version changed" };
122
+ if (!unchanged() || (authorization.profileId !== undefined && authorization.profileId !== relationship.profileId))
123
+ return { allowed: false, reason: "routine request binding changed during authorization" };
124
+ if (!event) {
125
+ if (!initialRequester || initialRequester.kind === "owner_event"
126
+ || (authorization.friendId !== undefined && authorization.friendId !== initialRequester.friendId)
127
+ || (authorization.requestId !== undefined && authorization.requestId !== initialRequester.requestId)
128
+ || (expected && !(0, node_util_1.isDeepStrictEqual)(expected.requester, initialRequester)))
129
+ return { allowed: false, reason: "routine request binding does not match live authorization" };
130
+ return { allowed: true, requester: initialRequester, receiptId: authorization.receiptId, profileVersion: authorization.profileVersion };
131
+ }
132
+ if (!sanctuary || authorization.profileId !== "sanctuary-event" || typeof authorization.friendId !== "string" || !authorization.friendId.trim())
133
+ return { allowed: false, reason: "current health event authorization is unavailable" };
134
+ let eventTarget;
135
+ if (expected) {
136
+ if (expected.requester.kind !== "owner_event" || expected.requester.target.name !== target)
137
+ return { allowed: false, reason: "current health event target binding changed" };
138
+ eventTarget = expected.requester.target;
139
+ }
140
+ else {
141
+ let listed;
142
+ try {
143
+ listed = await sanctuary.listContainers();
144
+ }
145
+ catch {
146
+ return { allowed: false, reason: "current health event container status is unavailable" };
147
+ }
148
+ if (!isRecord(listed) || listed.ok !== true || !isRecord(listed.data) || listed.data.truncated !== false || !Array.isArray(listed.data.containers))
149
+ return { allowed: false, reason: "current health event container status is invalid" };
150
+ const matches = listed.data.containers.filter((value) => isRecord(value) && value.name === target);
151
+ const container = matches[0];
152
+ if (matches.length !== 1 || !container || typeof container.id !== "string" || !container.id.trim()
153
+ || container.state !== "exited" || container.degraded !== false)
154
+ return { allowed: false, reason: "current health event target is not exactly stopped" };
155
+ eventTarget = { id: container.id, name: target };
156
+ }
157
+ const requester = routineActionRequester(ctx, { friendId: authorization.friendId, target: eventTarget });
158
+ if (!requester || requester.kind !== "owner_event" || !unchanged() || (expected && !(0, node_util_1.isDeepStrictEqual)(expected.requester, requester)))
159
+ return { allowed: false, reason: "current health event target or request binding changed" };
160
+ let current;
161
+ try {
162
+ current = (0, router_1.readExternalEventRecord)(requester.event.recordPath);
163
+ }
164
+ catch {
165
+ return { allowed: false, reason: "current health event record is unavailable" };
166
+ }
167
+ const lease = requester.event;
168
+ if (current.agent !== lease.agent || current.source !== lease.source || current.eventId !== lease.eventId || current.eventType !== "health.observed"
169
+ || current.generation !== lease.generation || current.observationRevision !== lease.observationRevision || current.claimOwner !== lease.claimOwner
170
+ || current.executionState !== "running" || !(Date.parse(current.claimExpiresAt ?? "") > Date.now())
171
+ || current.transition === "recovered" || current.pendingObservation !== null)
172
+ return { allowed: false, reason: "current health event claim or observation changed" };
173
+ return { allowed: true, requester, receiptId: authorization.receiptId, profileVersion: authorization.profileVersion };
174
+ }
47
175
  function renderRelationshipPreferences(friend) {
48
176
  const now = Date.now();
49
177
  return Object.entries(friend.relationshipPolicy?.preferences ?? {})
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = exports.renderInnerProgressStatus = exports.speakTool = exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = void 0;
4
- exports.routineActionRequester = routineActionRequester;
3
+ exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = exports.routineActionRequester = exports.renderInnerProgressStatus = exports.speakTool = exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = void 0;
4
+ exports.routineActionDenied = routineActionDenied;
5
+ const runtime_1 = require("../nerves/runtime");
5
6
  const tools_files_1 = require("./tools-files");
6
7
  const tools_shell_1 = require("./tools-shell");
7
8
  const tools_notes_1 = require("./tools-notes");
@@ -40,17 +41,12 @@ Object.defineProperty(exports, "speakTool", { enumerable: true, get: function ()
40
41
  // Re-export renderInnerProgressStatus for consumers
41
42
  var tools_session_2 = require("./tools-session");
42
43
  Object.defineProperty(exports, "renderInnerProgressStatus", { enumerable: true, get: function () { return tools_session_2.renderInnerProgressStatus; } });
43
- function routineActionRequester(ctx) {
44
- const actor = ctx?.relationshipAuthorization?.actor;
45
- if (!actor)
46
- return null;
47
- if (actor.trustLevel === "family")
48
- return { kind: "owner", friendId: actor.friendId };
49
- const requestId = ctx?.relationshipAuthorization?.requestId;
50
- const session = ctx?.currentSession;
51
- if (actor.trustLevel !== "friend" || !requestId || !session || session.friendId !== actor.friendId)
52
- return null;
53
- return { kind: "household_request", friendId: actor.friendId, requestId, origin: { friendId: session.friendId, channel: session.channel, key: session.key } };
44
+ var relationship_authorization_1 = require("./relationship-authorization");
45
+ Object.defineProperty(exports, "routineActionRequester", { enumerable: true, get: function () { return relationship_authorization_1.routineActionRequester; } });
46
+ function routineActionDenied(reason) {
47
+ const message = reason.slice(0, 500);
48
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.routine_action_denied", message: "routine action denied", meta: { reason: message } });
49
+ return JSON.stringify({ ok: false, error: { code: "routine_action_denied", message, degraded: true } });
54
50
  }
55
51
  // Tracks which file paths have been read via read_file in this session.
56
52
  // edit_file requires a file to be read first (must-read-first guard).
@@ -0,0 +1,202 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.rootHostToolDefinition = exports.RootHostAuthorizationError = void 0;
37
+ exports.authorizeRootHostContext = authorizeRootHostContext;
38
+ exports.validateRootHostToolArguments = validateRootHostToolArguments;
39
+ exports.selectRootHostTool = selectRootHostTool;
40
+ exports.authorizeRootHostToolInvocation = authorizeRootHostToolInvocation;
41
+ const path = __importStar(require("node:path"));
42
+ const node_util_1 = require("node:util");
43
+ const runtime_1 = require("../nerves/runtime");
44
+ const sanctuary_host_authority_1 = require("../heart/daemon/sanctuary-host-authority");
45
+ const session_events_1 = require("../heart/session-events");
46
+ const shared_turn_1 = require("../senses/shared-turn");
47
+ const relationship_authorization_1 = require("./relationship-authorization");
48
+ const tool_arguments_1 = require("./tool-arguments");
49
+ const NAME = "sanctuary_host_execute";
50
+ const bindings = new WeakMap();
51
+ const selections = new WeakMap();
52
+ class RootHostAuthorizationError extends Error {
53
+ }
54
+ exports.RootHostAuthorizationError = RootHostAuthorizationError;
55
+ function requireOwner(condition) {
56
+ if (!condition)
57
+ throw new RootHostAuthorizationError("Sanctuary root host authorization denied");
58
+ }
59
+ function ownerFriend(friend, port) {
60
+ requireOwner(friend?.admissionState === "active" && friend.trustLevel === "family"
61
+ && friend.initiativePolicy === "proactive" && friend.capabilityProfileId === "sanctuary-owner");
62
+ const identities = friend.externalIds.filter((identity) => identity.provider === "telegram-user" && identity.tenantId === port.pins.expectedBotId);
63
+ requireOwner(identities.length === 1 && identities[0].externalId === port.pins.expectedOwnerUserId);
64
+ }
65
+ function currentCoordinates(ctx) {
66
+ const root = ctx.rootHost;
67
+ const relationship = ctx.relationshipAuthorization;
68
+ const actor = relationship?.actor;
69
+ const session = ctx.currentSession;
70
+ const friend = ctx.context?.friend;
71
+ requireOwner(ctx.agentName === "sanctuary" && typeof ctx.agentRoot === "string" && path.isAbsolute(ctx.agentRoot)
72
+ && path.resolve(ctx.agentRoot) === ctx.agentRoot && path.basename(ctx.agentRoot) === "sanctuary.ouro");
73
+ requireOwner(root && root.port.isHealthy() && !ctx.currentExternalEvent);
74
+ requireOwner(relationship?.profileId === "sanctuary-owner" && relationship.advertisedToolNames?.includes(NAME));
75
+ ownerFriend(friend, root.port);
76
+ requireOwner(actor && actor.trustLevel === "family" && actor.friendId === friend.id && session?.friendId === friend.id
77
+ && session.channel === "telegram" && ctx.context.channel.channel === "telegram");
78
+ requireOwner([friend.id, actor.sessionEventId, relationship.requestId, session.key].every((value) => typeof value === "string" && value.trim().length > 0));
79
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)("sanctuary", friend.id, "telegram", session.key, ctx.agentRoot);
80
+ requireOwner(session.sessionPath === sessionPath && sessionPath.startsWith(`${ctx.agentRoot}${path.sep}`));
81
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
82
+ const latest = envelope && (0, session_events_1.selectEffectiveSessionEvents)(envelope.events).findLast((event) => event.role === "user");
83
+ requireOwner(latest && latest.id === actor.sessionEventId && latest.relations.references.includes(relationship.requestId));
84
+ const profile = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(ctx.agentRoot).profiles["sanctuary-owner"];
85
+ requireOwner(profile && Number.isSafeInteger(profile.version) && profile.version >= 9 && profile.toolNames.includes(NAME));
86
+ return {
87
+ agentRoot: ctx.agentRoot, friendId: friend.id, requestId: relationship.requestId, sessionKey: session.key,
88
+ sessionPath, sessionEventId: actor.sessionEventId, profileId: "sanctuary-owner", profileVersion: profile.version,
89
+ keyId: root.port.pins.expectedKeyId, publicKeyDigest: root.port.pins.expectedPublicKeyDigest, targetHost: root.port.pins.expectedTargetHost,
90
+ };
91
+ }
92
+ function observationAccepted(ctx) {
93
+ const root = ctx.rootHost;
94
+ return !!root.observation && root.port.acceptsObservation(root.observation)
95
+ && root.observation.keyId === root.port.pins.expectedKeyId
96
+ && root.observation.publicKeyDigest === root.port.pins.expectedPublicKeyDigest
97
+ && root.observation.targetHost === root.port.pins.expectedTargetHost;
98
+ }
99
+ async function authorizeRootHostContext(ctx, expected) {
100
+ const previous = ctx.rootHost?.binding;
101
+ const provenance = previous && bindings.get(previous);
102
+ if (previous)
103
+ bindings.delete(previous);
104
+ const coordinates = currentCoordinates(ctx);
105
+ const root = ctx.rootHost;
106
+ const port = root.port;
107
+ const relationship = ctx.relationshipAuthorization;
108
+ const store = ctx.friendStore;
109
+ requireOwner(store && (expected ? (0, node_util_1.isDeepStrictEqual)(coordinates, expected) : observationAccepted(ctx)));
110
+ const friend = await store.get(coordinates.friendId);
111
+ ownerFriend(friend, port);
112
+ requireOwner(friend.id === coordinates.friendId);
113
+ const decision = await relationship.authorizeTool(NAME, {});
114
+ requireOwner(decision.allowed && decision.profileId === coordinates.profileId && decision.profileVersion === coordinates.profileVersion
115
+ && decision.friendId === coordinates.friendId && decision.requestId === coordinates.requestId
116
+ && typeof decision.receiptId === "string" && decision.receiptId.trim().length > 0);
117
+ requireOwner(ctx.rootHost === root && root.port === port && root.binding === previous && ctx.relationshipAuthorization === relationship && ctx.friendStore === store
118
+ && (0, node_util_1.isDeepStrictEqual)(coordinates, currentCoordinates(ctx)) && (expected !== undefined || observationAccepted(ctx)));
119
+ const binding = previous && provenance?.port === root.port && provenance.recovery === (expected !== undefined) && (0, node_util_1.isDeepStrictEqual)(previous, coordinates)
120
+ ? previous : Object.freeze(coordinates);
121
+ bindings.set(binding, { port: root.port, recovery: expected !== undefined });
122
+ root.binding = binding;
123
+ return binding;
124
+ }
125
+ const printable = (maximum) => ({ type: "string", minLength: 1, maxLength: maximum, pattern: "^[\\x20-\\x7e]+$" });
126
+ const argumentsSchema = { type: "array", maxItems: 64, items: printable(512) };
127
+ exports.rootHostToolDefinition = (0, tool_arguments_1.freezeToolValue)({
128
+ tool: {
129
+ type: "function",
130
+ function: {
131
+ name: NAME,
132
+ description: "Propose one exact Sanctuary host command for root-owned owner approval. Nothing executes until root approval and signed permit verification. verification is optional and defaults to null.",
133
+ parameters: {
134
+ type: "object", additionalProperties: false,
135
+ required: ["targetHost", "targetResource", "command", "workingDirectoryProfile", "environmentProfile", "timeoutMs"],
136
+ properties: {
137
+ targetHost: printable(256), targetResource: printable(256),
138
+ command: {
139
+ oneOf: [
140
+ { type: "object", additionalProperties: false, required: ["kind", "executable", "arguments"], properties: {
141
+ kind: { const: "executable" }, executable: printable(512), arguments: argumentsSchema,
142
+ } },
143
+ { type: "object", additionalProperties: false, required: ["kind", "interpreter", "arguments", "script"], properties: {
144
+ kind: { const: "script" }, interpreter: printable(512), arguments: argumentsSchema,
145
+ script: { type: "string", minLength: 1, maxLength: 2048, pattern: "^[\\x0a\\x20-\\x7e]+$" },
146
+ } },
147
+ ],
148
+ },
149
+ workingDirectoryProfile: { const: "host.root.v1" }, environmentProfile: { const: "host.clean.v1" },
150
+ timeoutMs: { type: "integer", minimum: 1000, maximum: 900000 },
151
+ verification: {
152
+ type: "object", nullable: true, additionalProperties: false, required: ["profile", "expectedStateDigest"],
153
+ properties: { profile: { const: "file.digest.v1" }, expectedStateDigest: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" } },
154
+ },
155
+ },
156
+ },
157
+ },
158
+ },
159
+ requiredCapability: "approval-continuation",
160
+ summaryKeys: ["targetHost", "targetResource"],
161
+ riskProfile: { risk: "high", mutates: "external_side_effect", reason: "owner-approved arbitrary host command" },
162
+ approvalPolicy: () => ({ kind: "required", policyId: "sanctuary.host.owner-approved.v1", actionClass: "owner_approved_arbitrary_host", requiresSoleCall: true }),
163
+ handler: () => { throw new Error("Sanctuary host execution requires the root approval continuation; resident execution is forbidden"); },
164
+ });
165
+ function validateRootHostToolArguments(args, targetHost) {
166
+ requireOwner(!Object.hasOwn(args, "ownerObservation"));
167
+ const proposal = {
168
+ ...args, verification: args.verification === undefined ? null : args.verification,
169
+ ownerObservation: { digest: `sha256:${"0".repeat(64)}`, updateId: 0, userId: "1", chatId: "1", messageId: "1" },
170
+ };
171
+ // S4 owns command canonicalization. This placeholder only validates the model-owned fields.
172
+ (0, sanctuary_host_authority_1.validateHostProposalRequest)(proposal, targetHost);
173
+ const { ownerObservation: _observation, ...command } = proposal;
174
+ return structuredClone(command);
175
+ }
176
+ function selectRootHostTool(ctx, capabilities) {
177
+ try {
178
+ const binding = ctx?.rootHost?.binding;
179
+ const provenance = binding && bindings.get(binding);
180
+ if (!ctx || !binding || !provenance || provenance.port !== ctx.rootHost.port || !capabilities?.has("approval-continuation")
181
+ || !(0, node_util_1.isDeepStrictEqual)(binding, currentCoordinates(ctx)) || (!provenance.recovery && !observationAccepted(ctx)))
182
+ return undefined;
183
+ const selected = Object.freeze({ ...exports.rootHostToolDefinition });
184
+ selections.set(selected, { binding, capabilities });
185
+ return selected;
186
+ }
187
+ catch {
188
+ return undefined;
189
+ }
190
+ }
191
+ async function authorizeRootHostToolInvocation(ctx, definition, args) {
192
+ const selected = definition && selections.get(definition);
193
+ requireOwner(ctx && selected && ctx.rootHost?.binding === selected.binding && ctx.toolSelection?.ordinary.includes(definition));
194
+ if (!ctx.rootHost.port.isHealthy() && !await ctx.rootHost.port.refresh())
195
+ throw new Error("Sanctuary root host authority is unavailable");
196
+ requireOwner(selectRootHostTool(ctx, selected.capabilities));
197
+ const provenance = bindings.get(selected.binding);
198
+ await authorizeRootHostContext(ctx, provenance.recovery ? selected.binding : undefined);
199
+ requireOwner(selectRootHostTool(ctx, selected.capabilities));
200
+ validateRootHostToolArguments(args, selected.binding.targetHost);
201
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.sanctuary_host_invocation_admitted", message: "Owner host-tool invocation admitted to the root approval path" });
202
+ }
@@ -16,7 +16,7 @@ exports.stewardPolicyToolDefinition = {
16
16
  type: "function",
17
17
  function: {
18
18
  name: "steward_policy_manage",
19
- description: "Read or update the household steward's typed desired-state and routine-action policy. Updates require the current authenticated family request. expectedVersion is optional; omit it to apply against the current policy version, or supply a version from read for explicit stale-write detection.",
19
+ description: "Read or update the household steward's typed desired-state and routine-action policy. Updates require the current authenticated owner Telegram request and fresh authorization. expectedVersion is optional; omit it to apply against the current policy version, or supply a version from read for explicit stale-write detection.",
20
20
  parameters: {
21
21
  type: "object",
22
22
  properties: {
@@ -48,13 +48,20 @@ exports.stewardPolicyToolDefinition = {
48
48
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "read steward policy", meta: { action: "read" } });
49
49
  return JSON.stringify((0, steward_policy_1.readStewardPolicy)(ctx.agentRoot));
50
50
  }
51
- const actor = ctx.relationshipAuthorization?.actor;
51
+ const relationship = ctx.relationshipAuthorization;
52
+ const actor = relationship.actor;
52
53
  if (!actor)
53
54
  throw new Error("steward policy mutation requires authenticated relationship authority");
54
- const expectedVersion = args.expectedVersion === undefined
55
- ? (0, steward_policy_1.readStewardPolicy)(ctx.agentRoot).version
56
- : Number(args.expectedVersion);
57
- if (!Number.isInteger(expectedVersion) || expectedVersion < 0)
55
+ const { friendId, trustLevel, sessionEventId } = actor;
56
+ const { profileId, requestId } = relationship;
57
+ const sessionKey = ctx.currentSession?.key;
58
+ if (trustLevel !== "family" || profileId !== "sanctuary-owner" || typeof requestId !== "string" || !requestId.trim()
59
+ || typeof sessionKey !== "string" || !sessionKey.trim() || !sessionEventId.trim()
60
+ || ctx.currentSession?.friendId !== friendId || ctx.currentSession.channel !== "telegram" || ctx.currentExternalEvent) {
61
+ throw new Error("steward policy mutation requires a current authenticated owner Telegram request and session");
62
+ }
63
+ const expectedVersion = args.expectedVersion === undefined ? undefined : Number(args.expectedVersion);
64
+ if (expectedVersion !== undefined && (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0))
58
65
  throw new Error("expectedVersion must be a nonnegative integer");
59
66
  let mutation;
60
67
  if (args.action === "set_desired_state") {
@@ -85,11 +92,28 @@ exports.stewardPolicyToolDefinition = {
85
92
  }
86
93
  else
87
94
  throw new Error("steward policy action is invalid");
88
- const result = (0, steward_policy_1.updateStewardPolicy)(ctx.agentRoot, { expectedVersion, actor, mutation });
89
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "updated steward policy", meta: { action: args.action } });
90
- return JSON.stringify(result);
95
+ const agentRoot = ctx.agentRoot;
96
+ return Promise.resolve(relationship.authorizeTool("steward_policy_manage", args)).then((authorization) => {
97
+ if (!authorization.allowed)
98
+ throw new Error(`steward policy authorization denied: ${authorization.reason}`);
99
+ if (typeof authorization.profileVersion !== "number" || !Number.isSafeInteger(authorization.profileVersion) || authorization.profileVersion < 1 || !authorization.receiptId.trim()) {
100
+ throw new Error("steward policy owner authorization is not versioned");
101
+ }
102
+ if (ctx.relationshipAuthorization !== relationship || relationship.profileId !== profileId || relationship.requestId !== requestId
103
+ || relationship.actor?.friendId !== friendId || relationship.actor.trustLevel !== trustLevel || relationship.actor.sessionEventId !== sessionEventId
104
+ || ctx.currentSession?.friendId !== friendId || ctx.currentSession.key !== sessionKey || ctx.currentSession.channel !== "telegram" || ctx.currentExternalEvent) {
105
+ throw new Error("steward policy owner authorization or session changed before mutation");
106
+ }
107
+ const result = (0, steward_policy_1.updateStewardPolicy)(agentRoot, {
108
+ expectedVersion: expectedVersion ?? (0, steward_policy_1.readStewardPolicy)(agentRoot).version,
109
+ actor: { friendId, trustLevel, sessionEventId, authorization: { profileId, requestId, sessionKey, receiptId: authorization.receiptId, profileVersion: authorization.profileVersion } },
110
+ mutation,
111
+ });
112
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "updated steward policy", meta: { action: args.action } });
113
+ return JSON.stringify(result);
114
+ });
91
115
  },
92
116
  riskProfile: (args) => args.action === "read"
93
117
  ? { mutates: "none", risk: "low" }
94
- : { mutates: "durable_state_write", risk: "high", reason: "updates typed household steward policy under authenticated family authority" },
118
+ : { mutates: "durable_state_write", risk: "high", reason: "updates typed household steward policy under current authenticated owner authority" },
95
119
  };
@@ -7,6 +7,8 @@ const node_crypto_1 = require("node:crypto");
7
7
  const runtime_1 = require("../nerves/runtime");
8
8
  const unraid_client_1 = require("./unraid-client");
9
9
  const tools_base_1 = require("./tools-base");
10
+ const relationship_authorization_1 = require("./relationship-authorization");
11
+ const approval_store_1 = require("../heart/approval-store");
10
12
  const steward_policy_1 = require("../heart/steward-policy");
11
13
  const obligations_1 = require("../arc/obligations");
12
14
  exports.SANCTUARY_CONTAINERS_QUERY = `query SanctuaryContainers {
@@ -140,9 +142,11 @@ function liveSourceIdentityDigest(value) {
140
142
  return (0, node_crypto_1.createHash)("sha256").update(prefixedId).digest("hex");
141
143
  }
142
144
  function createUnraidReadTools(client) {
143
- const listContainers = async () => {
145
+ const listContainers = async (signal) => {
144
146
  try {
145
- const data = await client.read(exports.SANCTUARY_CONTAINERS_QUERY, {});
147
+ const data = signal
148
+ ? await client.read(exports.SANCTUARY_CONTAINERS_QUERY, {}, signal)
149
+ : await client.read(exports.SANCTUARY_CONTAINERS_QUERY, {});
146
150
  return { ok: true, data: mapContainers(data) };
147
151
  }
148
152
  catch (error) {
@@ -309,16 +313,14 @@ const emptyParameters = { type: "object", properties: {}, additionalProperties:
309
313
  function missingRuntime() {
310
314
  return JSON.stringify({ ok: false, error: { code: "invalid_response", message: "Sanctuary runtime is unavailable", degraded: true } });
311
315
  }
312
- function householdRepairObligation(ctx, target) {
313
- const requester = (0, tools_base_1.routineActionRequester)(ctx);
314
- if (requester?.kind !== "household_request")
316
+ function householdRepairObligation(selection, target) {
317
+ if (selection?.kind !== "standing" || selection.requester.kind !== "household_request")
315
318
  return null;
316
- if (!ctx.agentRoot)
317
- throw new Error("household repair request tracking is unavailable");
318
- const existing = (0, obligations_1.findPendingObligationForRequest)(ctx.agentRoot, { requestId: requester.requestId, owedTo: requester.origin });
319
+ const { agentRoot, requester } = selection;
320
+ const existing = (0, obligations_1.findPendingObligationForRequest)(agentRoot, { requestId: requester.requestId, owedTo: requester.origin });
319
321
  if (existing)
320
322
  return existing;
321
- return (0, obligations_1.createObligation)(ctx.agentRoot, {
323
+ return (0, obligations_1.createObligation)(agentRoot, {
322
324
  origin: requester.origin,
323
325
  owedTo: requester.origin,
324
326
  requestId: requester.requestId,
@@ -328,12 +330,12 @@ function householdRepairObligation(ctx, target) {
328
330
  nextAction: `Restart ${target} under the Butler's standing grant and verify recovery`,
329
331
  });
330
332
  }
331
- async function runTrackedRestart(ctx, target, routine) {
332
- const obligation = householdRepairObligation(ctx, target);
333
+ async function runTrackedRestart(ctx, target, execution) {
334
+ const obligation = householdRepairObligation(ctx.routineActionSelection, target);
333
335
  if (obligation && ctx.agentRoot)
334
336
  (0, obligations_1.advanceObligation)(ctx.agentRoot, obligation.id, { status: "updating_runtime", latestNote: `Starting the requested restart of ${target}` });
335
337
  try {
336
- const result = await ctx.sanctuary.restartContainer({ container: target }, routine ? { routine } : undefined);
338
+ const result = await ctx.sanctuary.restartContainer({ container: target }, execution);
337
339
  if (obligation && ctx.agentRoot) {
338
340
  const succeeded = !!result && typeof result === "object" && !Array.isArray(result) && result.ok === true;
339
341
  if (succeeded)
@@ -439,33 +441,78 @@ exports.unraidToolDefinitions = [
439
441
  },
440
442
  },
441
443
  handler: async (args, ctx) => {
444
+ if (ctx?.routineActionSelection?.kind === "denied")
445
+ return (0, tools_base_1.routineActionDenied)(ctx.routineActionSelection.reason);
442
446
  if (!ctx?.sanctuary)
443
447
  return missingRuntime();
448
+ if (!ctx.routineActionSelection && !ctx.restartApproval && (ctx.relationshipAuthorization || ctx.currentExternalEvent))
449
+ return (0, tools_base_1.routineActionDenied)("routine action requires its standing selection or owner approval");
444
450
  const target = String(args.container);
445
- let routine;
451
+ let execution;
452
+ if (ctx.restartApproval) {
453
+ const approval = ctx.restartApproval;
454
+ const unchanged = () => ctx.restartApproval === approval && !ctx.routineActionSelection && !ctx.currentExternalEvent
455
+ && ctx.agentRoot === approval.agentRoot && !!approval.ownerBinding && !!approval.target && approval.target.name === target
456
+ && !!approval.sessionPath && ctx.currentSession?.sessionPath === approval.sessionPath;
457
+ const reauthorize = async () => {
458
+ if (!unchanged())
459
+ return { allowed: false, reason: "restart approval binding changed" };
460
+ const authorization = await (0, relationship_authorization_1.authorizeRestartApprovalRequester)(ctx, args, approval.ownerBinding);
461
+ if (!authorization.allowed)
462
+ return authorization;
463
+ if (!unchanged())
464
+ return { allowed: false, reason: "restart approval binding changed" };
465
+ if ((0, approval_store_1.canonicalApprovalArguments)(args).digest !== approval.argumentDigest)
466
+ return { allowed: false, reason: "restart approval arguments changed" };
467
+ return authorization;
468
+ };
469
+ const authorization = await reauthorize();
470
+ if (!authorization.allowed)
471
+ return (0, tools_base_1.routineActionDenied)(authorization.reason);
472
+ execution = {
473
+ approval: {
474
+ target: approval.target,
475
+ reauthorize: async () => {
476
+ const current = await reauthorize();
477
+ if (!current.allowed)
478
+ return current;
479
+ const decision = (0, steward_policy_1.inspectRoutineActionGrant)(approval.agentRoot, {
480
+ key: `unraid.restart:${target}`, action: "unraid.container.restart", target,
481
+ requester: current.requester, authorizationVersion: current.profileVersion,
482
+ });
483
+ if (decision.allowed || !decision.approvalFallback)
484
+ return { allowed: false, reason: decision.allowed ? "restart approval no longer has owner fallback" : decision.reason };
485
+ return { allowed: true };
486
+ },
487
+ },
488
+ };
489
+ }
446
490
  if (ctx.routineActionSelection) {
447
- if (!ctx.agentRoot || !(0, tools_base_1.routineActionRequester)(ctx))
448
- return JSON.stringify({ ok: false, error: { code: "approval_required", message: "routine action authorization is unavailable", degraded: true } });
449
- const { key, expectedPolicyVersion } = ctx.routineActionSelection;
491
+ if (!ctx.agentRoot)
492
+ return (0, tools_base_1.routineActionDenied)("routine action authorization is unavailable");
493
+ if (ctx.agentRoot !== ctx.routineActionSelection.agentRoot)
494
+ return (0, tools_base_1.routineActionDenied)("routine action agent root changed");
495
+ const { key, expectedPolicyVersion, expectedDesiredStateVersion, expectedGrantVersion, requester, authorizationVersion } = ctx.routineActionSelection;
450
496
  if (ctx.routineActionSelection.target !== target)
451
- return JSON.stringify({ ok: false, error: { code: "approval_required", message: "routine action arguments changed", degraded: true } });
452
- const decision = (0, steward_policy_1.inspectRoutineActionGrant)(ctx.agentRoot, { key, action: "unraid.container.restart", target, expectedPolicyVersion });
497
+ return (0, tools_base_1.routineActionDenied)("routine action arguments changed");
498
+ const decision = (0, steward_policy_1.inspectRoutineActionGrant)(ctx.agentRoot, { key, action: "unraid.container.restart", target, expectedPolicyVersion, expectedDesiredStateVersion, expectedGrantVersion, requester, authorizationVersion });
453
499
  if (!decision.allowed)
454
- return JSON.stringify({ ok: false, error: { code: "approval_required", message: decision.reason, degraded: true } });
455
- routine = {
456
- key,
457
- expectedPolicyVersion: decision.policyVersion,
458
- reauthorize: async () => {
459
- const authorization = await ctx.relationshipAuthorization.authorizeTool("unraid_restart_container", { container: target });
460
- if (!authorization.allowed)
461
- return authorization;
462
- if (!Number.isInteger(authorization.profileVersion) || Number(authorization.profileVersion) < 1)
463
- return { allowed: false, reason: "relationship capability profile is not versioned" };
464
- return { allowed: true, receiptId: authorization.receiptId, profileVersion: Number(authorization.profileVersion) };
465
- },
466
- };
500
+ return (0, tools_base_1.routineActionDenied)(decision.reason);
501
+ execution = { routine: {
502
+ key,
503
+ expectedPolicyVersion: decision.policyVersion,
504
+ expectedDesiredStateVersion: decision.desiredStateVersion,
505
+ expectedGrantVersion: decision.grantVersion,
506
+ requester: structuredClone(decision.requester),
507
+ reauthorize: async () => {
508
+ const authorization = await (0, relationship_authorization_1.authorizeRoutineActionRequester)(ctx, { container: target }, { requester, profileVersion: authorizationVersion });
509
+ if (!authorization.allowed)
510
+ return authorization;
511
+ return { allowed: true, receiptId: authorization.receiptId, profileVersion: authorization.profileVersion };
512
+ },
513
+ } };
467
514
  }
468
- return JSON.stringify(await runTrackedRestart(ctx, target, routine));
515
+ return JSON.stringify(await runTrackedRestart(ctx, target, execution));
469
516
  },
470
517
  riskProfile: { mutates: "external_side_effect", risk: "high", reason: "restarts one existing allowlisted Docker container" },
471
518
  approvalPolicy: () => ({