@ouro.bot/cli 0.1.0-alpha.816 → 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 (49) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +10 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +280 -60
  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/core.js +2 -1
  16. package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
  17. package/dist/heart/daemon/container-spec-auditor.js +16 -9
  18. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  19. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  20. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  21. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  22. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  23. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  24. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  25. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  26. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  27. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  28. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  29. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  30. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  31. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  33. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  36. package/dist/heart/daemon/sense-manager.js +20 -10
  37. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  38. package/dist/repertoire/tools.js +15 -6
  39. package/dist/senses/root-host-approval-port.js +345 -0
  40. package/dist/senses/root-host-approval-runtime.js +393 -0
  41. package/dist/senses/sanctuary-authority-resident.js +102 -0
  42. package/dist/senses/telegram-admission.js +7 -0
  43. package/dist/senses/telegram-attachments.js +5 -1
  44. package/dist/senses/telegram-authority-transport.js +231 -0
  45. package/dist/senses/telegram-client.js +31 -8
  46. package/dist/senses/telegram-entry.js +1 -1
  47. package/dist/senses/telegram.js +168 -28
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
@@ -161,6 +161,9 @@ function runtimeConfigUnavailableDetail(agent, runtimeConfig) {
161
161
  return `missing vault ${itemName} (${agent})`;
162
162
  return `vault ${itemName} unavailable (${compactRuntimeConfigError(agent, runtimeConfig.error)})`;
163
163
  }
164
+ function needsMachineRuntimeConfig(agent, sense) {
165
+ return sense === "bluebubbles" || sense === "voice" || sense === "a2a" || (agent === "sanctuary" && sense === "telegram");
166
+ }
164
167
  function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntimeConfig = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent), workbenchHasStaleBundleEntry = false) {
165
168
  const base = {
166
169
  cli: { configured: true, detail: "local interactive terminal" },
@@ -183,7 +186,14 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
183
186
  const integrations = payload.integrations;
184
187
  const voice = machinePayload.voice;
185
188
  const a2a = machinePayload.a2a;
186
- if (senses.telegram.enabled) {
189
+ if (senses.telegram.enabled && agent === "sanctuary") {
190
+ const ready = runtimeConfig.ok && machineRuntimeConfig.ok
191
+ && !Object.hasOwn(payload, "telegramBotToken") && !Object.hasOwn(machinePayload, "telegramBotToken");
192
+ base.telegram = ready
193
+ ? { configured: true, detail: "root authority transport; gateway validated at process startup" }
194
+ : { configured: false, detail: "root authority migration requires available tokenless credential inventories" };
195
+ }
196
+ else if (senses.telegram.enabled) {
187
197
  const missing = ["telegramBotToken", "telegramAuthorizedUserId", "telegramAuthorizedChatId"]
188
198
  .filter((field) => !textField(payload, field));
189
199
  base.telegram = missing.length === 0
@@ -355,7 +365,9 @@ function senseRepairHint(agent, sense) {
355
365
  return `Agent-runnable: run 'ouro connect a2a --agent ${agent}', then restart with 'ouro up'.`;
356
366
  }
357
367
  if (sense === "telegram") {
358
- return `Agent-runnable: store Telegram bot/user/chat coordinates with 'ouro connect telegram --agent ${agent}', then restart with 'ouro up'.`;
368
+ return agent === "sanctuary"
369
+ ? "Agent-runnable: complete or recover the root authority migration; do not restore a resident Telegram token."
370
+ : `Agent-runnable: store Telegram bot/user/chat coordinates with 'ouro connect telegram --agent ${agent}', then restart with 'ouro up'.`;
359
371
  }
360
372
  /* v8 ignore next -- Workbench is deliberately not daemon-managed, so getSenseInventory never asks the daemon manager for a repair hint @preserve */
361
373
  if (sense === "workbench") {
@@ -655,10 +667,10 @@ class DaemonSenseManager {
655
667
  this.configRefreshRetryTimers.delete(name);
656
668
  this.configRefreshRetryAttempts.delete(name);
657
669
  }
658
- shouldRetryConfigRefresh(sense, runtimeConfig, machineRuntimeConfig) {
670
+ shouldRetryConfigRefresh(agent, sense, runtimeConfig, machineRuntimeConfig) {
659
671
  if (!runtimeConfig.ok && runtimeConfig.reason === "unavailable")
660
672
  return true;
661
- if ((sense === "bluebubbles" || sense === "voice" || sense === "a2a") &&
673
+ if (needsMachineRuntimeConfig(agent, sense) &&
662
674
  !machineRuntimeConfig.ok &&
663
675
  machineRuntimeConfig.reason === "unavailable")
664
676
  return true;
@@ -668,7 +680,7 @@ class DaemonSenseManager {
668
680
  let retryAfterMs = null;
669
681
  try {
670
682
  const refreshed = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
671
- const machineRefreshed = parsed.sense === "bluebubbles" || parsed.sense === "voice" || parsed.sense === "a2a"
683
+ const machineRefreshed = needsMachineRuntimeConfig(parsed.agent, parsed.sense)
672
684
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
673
685
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
674
686
  const context = this.contexts.get(parsed.agent);
@@ -677,7 +689,7 @@ class DaemonSenseManager {
677
689
  return;
678
690
  context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed, context.workbenchHasStaleBundleEntry);
679
691
  if (!context.facts[parsed.sense].configured) {
680
- if (this.shouldRetryConfigRefresh(parsed.sense, refreshed, machineRefreshed)) {
692
+ if (this.shouldRetryConfigRefresh(parsed.agent, parsed.sense, refreshed, machineRefreshed)) {
681
693
  retryAfterMs = this.nextConfigRetryDelayMs(name);
682
694
  }
683
695
  else {
@@ -723,13 +735,11 @@ class DaemonSenseManager {
723
735
  /* v8 ignore next -- periodic refresh work only exists when a managed background sense is enabled @preserve */
724
736
  if (enabledManagedSenses.length === 0)
725
737
  return;
726
- /* v8 ignore start -- periodic freshness refresh uses the same runtime readers covered by startup integration tests @preserve */
727
738
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
728
- const needsMachineConfig = enabledManagedSenses.some((sense) => sense === "bluebubbles" || sense === "voice" || sense === "a2a");
739
+ const needsMachineConfig = enabledManagedSenses.some((sense) => needsMachineRuntimeConfig(agent, sense));
729
740
  const machineRuntimeConfig = needsMachineConfig
730
741
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(), { preserveCachedOnFailure: true })
731
742
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
732
- /* v8 ignore stop */
733
743
  context.facts = senseFactsFromRuntimeConfig(agent, context.senses, runtimeConfig, machineRuntimeConfig, context.workbenchHasStaleBundleEntry);
734
744
  });
735
745
  await Promise.all(refreshes);
@@ -835,7 +845,7 @@ class DaemonSenseManager {
835
845
  return null;
836
846
  const managedName = `${parsed.agent}:${parsed.sense}`;
837
847
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
838
- const needsMachineConfig = parsed.sense === "bluebubbles" || parsed.sense === "voice" || parsed.sense === "a2a";
848
+ const needsMachineConfig = needsMachineRuntimeConfig(parsed.agent, parsed.sense);
839
849
  const machineRuntimeConfig = needsMachineConfig
840
850
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
841
851
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
@@ -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
+ }
@@ -39,6 +39,7 @@ const tools_steward_policy_1 = require("./tools-steward-policy");
39
39
  const steward_policy_1 = require("../heart/steward-policy");
40
40
  const relationship_authorization_1 = require("./relationship-authorization");
41
41
  const tool_approval_1 = require("../heart/tool-approval");
42
+ const tools_sanctuary_host_1 = require("./tools-sanctuary-host");
42
43
  function safeGetAgentRoot() {
43
44
  try {
44
45
  return (0, identity_1.getAgentRoot)();
@@ -120,7 +121,7 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
120
121
  const mcp = mcpManager ? (0, mcp_tools_1.mcpToolsAsDefinitions)(mcpManager) : [];
121
122
  const native = [...tools_base_1.baseToolDefinitions, ...additionalDefinitions];
122
123
  assertUniqueToolSchemas([
123
- ...native.map((definition) => definition.tool), ...mcp.map((definition) => definition.tool),
124
+ ...native.map((definition) => definition.tool), tools_sanctuary_host_1.rootHostToolDefinition.tool, ...mcp.map((definition) => definition.tool),
124
125
  tools_flow_1.ponderTool, tools_flow_1.restTool, tools_flow_1.observeTool, tools_flow_1.settleTool, tools_flow_1.speakTool,
125
126
  ]);
126
127
  let ordinary;
@@ -148,6 +149,11 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
148
149
  ...integrations, ...mcp,
149
150
  ];
150
151
  }
152
+ const hostTool = capabilities?.channel === "telegram"
153
+ ? (0, tools_sanctuary_host_1.selectRootHostTool)({ ...context, context: _context ?? context?.context }, providerCapabilities)
154
+ : undefined;
155
+ if (hostTool)
156
+ ordinary.push(hostTool);
151
157
  const inner = capabilities?.channel === "inner";
152
158
  const habit = context?.habitSession;
153
159
  if (inner) {
@@ -166,7 +172,7 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
166
172
  ].filter((tool) => (!sanctuary || (knownSanctuaryProfile && tool.function.name !== "observe"))
167
173
  && (!relationship || relationship.advertisedToolNames.includes(tool.function.name)));
168
174
  return Object.freeze({
169
- ordinary: Object.freeze(ordinary.map((definition) => Object.freeze({
175
+ ordinary: Object.freeze(ordinary.map((definition) => definition === hostTool ? definition : Object.freeze({
170
176
  ...definition,
171
177
  tool: (0, tool_arguments_1.freezeToolValue)(structuredClone(definition.tool)),
172
178
  ...(definition.riskProfile && typeof definition.riskProfile !== "function"
@@ -328,6 +334,8 @@ async function preflightToolCall(name, args, ctx) {
328
334
  (0, runtime_1.emitNervesEvent)({ level: "error", event: "tool.error", component: "tools", message: "unknown or unselected tool requested", meta: { name } });
329
335
  return { kind: "rejected_before_handler", text: `unknown: ${name}` };
330
336
  }
337
+ if (name === tools_sanctuary_host_1.rootHostToolDefinition.tool.function.name)
338
+ await (0, tools_sanctuary_host_1.authorizeRootHostToolInvocation)(ctx, def, args);
331
339
  if (selection || ctx?.relationshipAuthorization) {
332
340
  const validation = (0, tool_arguments_1.validateAdvertisedToolArguments)(JSON.stringify(args), schema.function.parameters ?? {});
333
341
  if (!validation.ok)
@@ -338,7 +346,7 @@ async function preflightToolCall(name, args, ctx) {
338
346
  return { kind: "rejected_before_handler", text: (0, tools_base_1.routineActionDenied)(ctx.routineActionSelection.reason) };
339
347
  }
340
348
  const relationship = ctx?.relationshipAuthorization;
341
- const decision = await relationship?.authorizeTool(name, args);
349
+ const decision = name === tools_sanctuary_host_1.rootHostToolDefinition.tool.function.name ? undefined : await relationship?.authorizeTool(name, args);
342
350
  if (decision && (!decision.allowed || !relationship.advertisedToolNames.includes(name)
343
351
  || (decision.profileId !== undefined && decision.profileId !== relationship.profileId))) {
344
352
  const reason = decision.allowed ? "current relationship selection changed" : decision.reason;
@@ -349,9 +357,10 @@ async function preflightToolCall(name, args, ctx) {
349
357
  return { kind: "rejected_before_handler", text: `relationship authorization required: ${reason}` };
350
358
  }
351
359
  const currentSelection = ctx?.selectCurrentTools?.()
352
- ?? (relationship?.profileId?.startsWith("sanctuary-") || (relationship && ctx?.agentName === "sanctuary")
353
- ? selectToolsForChannel(undefined, undefined, undefined, undefined, undefined, undefined, ctx)
354
- : undefined);
360
+ ?? (name === tools_sanctuary_host_1.rootHostToolDefinition.tool.function.name ? selection
361
+ : relationship?.profileId?.startsWith("sanctuary-") || (relationship && ctx?.agentName === "sanctuary")
362
+ ? selectToolsForChannel(undefined, undefined, undefined, undefined, undefined, undefined, ctx)
363
+ : undefined);
355
364
  const current = currentSelection ? findDefinition(name, currentSelection) : def?.mcpBinding ? def : findDefinition(name);
356
365
  const currentSchema = current?.tool ?? currentSelection?.engine.find((tool) => tool.function.name === name)
357
366
  ?? (!currentSelection ? selection?.engine.find((tool) => tool.function.name === name) : undefined);