@ouro.bot/cli 0.1.0-alpha.816 → 0.1.0-alpha.818
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/sanctuary-host-launcher.sh +16 -0
- package/changelog.json +16 -0
- package/deploy/unraid/Dockerfile +6 -0
- package/deploy/unraid/README.txt +296 -62
- package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
- package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
- package/deploy/unraid/sanctuary-authority-installation.json +36 -0
- package/deploy/unraid/sanctuary-authority-service.sh +30 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
- package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/core.js +2 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +8 -7
- package/dist/heart/daemon/container-spec-auditor.js +16 -9
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
- package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
- package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
- package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
- package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
- package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
- package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
- package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
- package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
- package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
- package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
- package/dist/heart/daemon/sense-manager.js +20 -10
- package/dist/repertoire/tools-sanctuary-host.js +202 -0
- package/dist/repertoire/tools.js +15 -6
- package/dist/senses/private-runtime.js +2 -2
- package/dist/senses/root-host-approval-port.js +345 -0
- package/dist/senses/root-host-approval-runtime.js +393 -0
- package/dist/senses/sanctuary-authority-resident.js +102 -0
- package/dist/senses/telegram-admission.js +7 -0
- package/dist/senses/telegram-attachments.js +5 -1
- package/dist/senses/telegram-authority-transport.js +231 -0
- package/dist/senses/telegram-client.js +31 -8
- package/dist/senses/telegram-entry.js +1 -1
- package/dist/senses/telegram.js +168 -28
- package/npm-shrinkwrap.json +2 -2
- 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
|
|
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 ((
|
|
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.
|
|
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) =>
|
|
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.
|
|
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
|
+
}
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -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
|
-
?? (
|
|
353
|
-
|
|
354
|
-
|
|
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);
|
|
@@ -491,14 +491,14 @@ function privateTurnExecutionPath(decision) {
|
|
|
491
491
|
}
|
|
492
492
|
function claimPrivateTurnExecution(decision, now) {
|
|
493
493
|
const executionPath = privateTurnExecutionPath(decision);
|
|
494
|
-
fs.mkdirSync(path.dirname(executionPath), { recursive: true });
|
|
494
|
+
fs.mkdirSync(path.dirname(executionPath), { recursive: true, mode: 0o700 });
|
|
495
495
|
try {
|
|
496
496
|
fs.writeFileSync(executionPath, JSON.stringify({
|
|
497
497
|
receiptId: decision.receiptId,
|
|
498
498
|
idempotencyKey: decision.idempotencyKey,
|
|
499
499
|
requestFingerprint: decision.requestFingerprint,
|
|
500
500
|
claimedAt: now().toISOString(),
|
|
501
|
-
}), { encoding: "utf-8", flag: "wx" });
|
|
501
|
+
}), { encoding: "utf-8", flag: "wx", mode: 0o600 });
|
|
502
502
|
return true;
|
|
503
503
|
}
|
|
504
504
|
catch (error) {
|