@ouro.bot/cli 0.1.0-alpha.814 → 0.1.0-alpha.816
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/changelog.json +15 -0
- package/deploy/unraid/README.txt +25 -305
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/approval-store.js +11 -1
- package/dist/heart/daemon/container-spec-auditor-main.js +3 -3
- package/dist/heart/daemon/container-spec-auditor.js +3 -17
- package/dist/heart/external-events/router.js +31 -15
- package/dist/heart/steward-policy.js +374 -58
- package/dist/heart/tool-approval.js +8 -1
- package/dist/repertoire/relationship-authorization.js +128 -0
- package/dist/repertoire/tools-base.js +9 -13
- package/dist/repertoire/tools-steward-policy.js +34 -10
- package/dist/repertoire/tools-unraid.js +79 -32
- package/dist/repertoire/tools.js +22 -19
- package/dist/repertoire/unraid-restart.js +179 -52
- package/dist/senses/private-runtime.js +27 -12
- package/dist/senses/sanctuary-health-runner.js +0 -1
- package/dist/senses/sanctuary-interactive-control.js +160 -56
- package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
- package/dist/senses/sanctuary-runtime.js +2 -0
- package/dist/senses/telegram-approval-runtime.js +130 -12
- package/dist/senses/telegram.js +30 -21
- package/npm-shrinkwrap.json +2 -2
- 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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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).
|
|
@@ -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
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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)(
|
|
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,
|
|
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 },
|
|
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
|
|
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
|
|
448
|
-
return
|
|
449
|
-
|
|
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
|
|
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
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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,
|
|
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: () => ({
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -37,6 +37,7 @@ const tools_unraid_1 = require("./tools-unraid");
|
|
|
37
37
|
const tools_flow_1 = require("./tools-flow");
|
|
38
38
|
const tools_steward_policy_1 = require("./tools-steward-policy");
|
|
39
39
|
const steward_policy_1 = require("../heart/steward-policy");
|
|
40
|
+
const relationship_authorization_1 = require("./relationship-authorization");
|
|
40
41
|
const tool_approval_1 = require("../heart/tool-approval");
|
|
41
42
|
function safeGetAgentRoot() {
|
|
42
43
|
try {
|
|
@@ -200,34 +201,33 @@ function resolveToolDefinition(toolName, selection) {
|
|
|
200
201
|
function approvalPolicyForToolName(name, args, selection) {
|
|
201
202
|
return resolveToolDefinition(name, selection)?.approvalPolicy?.(args) ?? { kind: "not_required" };
|
|
202
203
|
}
|
|
203
|
-
async function routineActionInvocation(
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
if (!authorization.allowed || !Number.isInteger(authorization.profileVersion) || Number(authorization.profileVersion) < 1)
|
|
218
|
-
return null;
|
|
204
|
+
async function routineActionInvocation(args, ctx) {
|
|
205
|
+
const agentRoot = ctx?.agentRoot;
|
|
206
|
+
const relationship = ctx?.relationshipAuthorization;
|
|
207
|
+
const sanctuary = ctx?.sanctuary;
|
|
208
|
+
const target = typeof args?.container === "string" ? args.container : "";
|
|
209
|
+
const authorization = await (0, relationship_authorization_1.authorizeRoutineActionRequester)(ctx, args);
|
|
210
|
+
if (!authorization.allowed)
|
|
211
|
+
return { kind: "denied", reason: authorization.reason };
|
|
212
|
+
const currentRequester = (0, tools_base_1.routineActionRequester)(ctx, authorization.requester.kind === "owner_event" ? { friendId: authorization.requester.friendId, target: authorization.requester.target } : undefined);
|
|
213
|
+
if (!ctx || !agentRoot || ctx.agentRoot !== agentRoot || ctx.relationshipAuthorization !== relationship || ctx.sanctuary !== sanctuary
|
|
214
|
+
|| args.container !== target || Object.keys(args).length !== 1 || !(0, node_util_1.isDeepStrictEqual)(currentRequester, authorization.requester))
|
|
215
|
+
return { kind: "denied", reason: "routine request binding changed after authorization" };
|
|
219
216
|
const key = `unraid.restart:${target}`;
|
|
220
|
-
const decision = (0, steward_policy_1.inspectRoutineActionGrant)(
|
|
221
|
-
|
|
217
|
+
const decision = (0, steward_policy_1.inspectRoutineActionGrant)(agentRoot, { key, action: "unraid.container.restart", target, requester: authorization.requester, authorizationVersion: authorization.profileVersion });
|
|
218
|
+
if (!decision.allowed)
|
|
219
|
+
return decision.approvalFallback ? null : { kind: "denied", reason: decision.reason };
|
|
220
|
+
return Object.freeze({ kind: "standing", agentRoot, key, target, expectedPolicyVersion: decision.policyVersion, expectedDesiredStateVersion: decision.desiredStateVersion, expectedGrantVersion: decision.grantVersion, requester: structuredClone(decision.requester), authorizationVersion: decision.authorizationVersion });
|
|
222
221
|
}
|
|
223
222
|
async function classifyApprovalForInvocation(name, args, ctx) {
|
|
224
223
|
const fallback = approvalPolicyForToolName(name, args, ctx?.toolSelection);
|
|
225
224
|
if (name !== "unraid_restart_container" || fallback.kind !== "required")
|
|
226
225
|
return { policy: fallback };
|
|
227
|
-
const routineActionSelection = await routineActionInvocation(
|
|
226
|
+
const routineActionSelection = await routineActionInvocation(args, ctx);
|
|
228
227
|
return routineActionSelection ? { policy: { kind: "not_required" }, routineActionSelection } : { policy: fallback };
|
|
229
228
|
}
|
|
230
229
|
async function approvalPolicyForInvocation(name, args, ctx) {
|
|
230
|
+
// An approval requirement is not execution authority; dispatch needs the full classification.
|
|
231
231
|
return (await classifyApprovalForInvocation(name, args, ctx)).policy;
|
|
232
232
|
}
|
|
233
233
|
const findDefinition = resolveToolDefinition;
|
|
@@ -334,6 +334,9 @@ async function preflightToolCall(name, args, ctx) {
|
|
|
334
334
|
return { kind: "rejected_before_handler", text: `invalid tool arguments: ${validation.reason}` };
|
|
335
335
|
}
|
|
336
336
|
(0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
|
|
337
|
+
if (name === "unraid_restart_container" && ctx?.routineActionSelection?.kind === "denied") {
|
|
338
|
+
return { kind: "rejected_before_handler", text: (0, tools_base_1.routineActionDenied)(ctx.routineActionSelection.reason) };
|
|
339
|
+
}
|
|
337
340
|
const relationship = ctx?.relationshipAuthorization;
|
|
338
341
|
const decision = await relationship?.authorizeTool(name, args);
|
|
339
342
|
if (decision && (!decision.allowed || !relationship.advertisedToolNames.includes(name)
|