@ouro.bot/cli 0.1.0-alpha.982 → 0.1.0-alpha.984

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.
@@ -33,9 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.OURO_CLI_TRUST_MANIFEST = void 0;
36
+ exports.OURO_CLI_TRUST_MANIFEST = exports.PSYCHE_REFUSAL = void 0;
37
37
  exports.canonicalTarget = canonicalTarget;
38
38
  exports.insideAgentFolder = insideAgentFolder;
39
+ exports.psycheFileClass = psycheFileClass;
40
+ exports.psycheWriteAllowed = psycheWriteAllowed;
41
+ exports.psycheEditNote = psycheEditNote;
39
42
  exports.guardInvocation = guardInvocation;
40
43
  const fs = __importStar(require("node:fs"));
41
44
  const os = __importStar(require("node:os"));
@@ -53,7 +56,6 @@ const REASONS = {
53
56
  readBeforeEdit: "i need to read that file first before i can edit it.",
54
57
  readBeforeOverwrite: "i need to read that file first before i can overwrite it.",
55
58
  protectedPath: "that path is protected — i can read it but not modify it.",
56
- psychePath: "my psyche files ship from the repository through a pull request and the replay gate; i can read them but not change them. i file the change request with Claude Code instead.",
57
59
  destructiveCommand: "that command is too dangerous to run — it could cause irreversible damage.",
58
60
  // Trust reasons (vary by relationship)
59
61
  needsTrust: "i'd need a closer friend to vouch for you before i can do that.",
@@ -133,7 +135,67 @@ function insideAgentFolder(p, agentRoot, folder) {
133
135
  const folders = [lexicalFolder, canonicalize(lexicalFolder)];
134
136
  return canonicalTarget(p, agentRoot).some((target) => folders.some((dir) => target === dir || target.startsWith(`${dir}${path.sep}`)));
135
137
  }
136
- /** The agent's psyche folder ships only from the repository (a pull request and the replay gate), so no tool writes into it. The shell check uses this lexical form; the file tools use `insideAgentFolder`. */
138
+ // --- the psyche belongs to the agent ---
139
+ // Every agent writes its own TACIT.md: what it learns about how its house works. The rest of its psyche (who it is) changes only
140
+ // when a family human asks it directly, one to one, on a channel that proves who they are.
141
+ exports.PSYCHE_REFUSAL = "that file is part of me; I change it when my owner asks me in our own chat.";
142
+ const PSYCHE_IDENTITY_CHANNELS = new Set(["telegram", "bluebubbles", "teams"]);
143
+ /** `tacit` for the agent's psyche/TACIT.md, `identity` for anything else in psyche/ (the folder and new files included), `null` outside it. */
144
+ function psycheFileClass(filePath, agentRoot) {
145
+ if (!filePath || !agentRoot || !insideAgentFolder(filePath, agentRoot, "psyche"))
146
+ return null;
147
+ const lexicalFolder = path.resolve(agentRoot, "psyche");
148
+ const folders = [lexicalFolder, canonicalize(lexicalFolder)];
149
+ const tacits = folders.map((folder) => path.join(folder, "TACIT.md"));
150
+ // Every form of the path that lands in psyche/ must be TACIT: a link that sends one form of it to SOUL makes it identity.
151
+ const inPsyche = canonicalTarget(filePath, agentRoot).filter((target) => folders.some((dir) => target === dir || target.startsWith(`${dir}${path.sep}`)));
152
+ return inPsyche.every((target) => tacits.includes(target)) ? "tacit" : "identity";
153
+ }
154
+ /** The lstat of `p`, or null when it does not exist; throws on any other lookup failure. */
155
+ function lstatIfPresent(p) {
156
+ try {
157
+ return fs.lstatSync(p);
158
+ }
159
+ catch (error) {
160
+ if (error.code === "ENOENT")
161
+ return null;
162
+ throw error;
163
+ }
164
+ }
165
+ /** Whether the person in this conversation may have the agent write this psyche file. */
166
+ function psycheWriteAllowed(fileClass, _filePath, context) {
167
+ const agentRoot = context.agentRoot;
168
+ /* v8 ignore next -- a file is only classed as psyche when there is an agent root @preserve */
169
+ if (!agentRoot)
170
+ return false;
171
+ try {
172
+ if (lstatIfPresent(path.join(agentRoot, "psyche"))?.isSymbolicLink())
173
+ return false;
174
+ if (fileClass === "tacit") {
175
+ // A symlink or a hard link would carry the write to some other file.
176
+ const tacit = lstatIfPresent(path.join(agentRoot, "psyche", "TACIT.md"));
177
+ if (tacit && (tacit.isSymbolicLink() || tacit.nlink > 1))
178
+ return false;
179
+ if (context.friendId === undefined)
180
+ return true;
181
+ return context.trustLevel === undefined || context.trustLevel === "friend" || context.trustLevel === "family";
182
+ }
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ return context.friendKind === "human"
188
+ && context.trustLevel === "family"
189
+ && context.channel !== undefined && PSYCHE_IDENTITY_CHANNELS.has(context.channel)
190
+ && context.isGroupChat !== true
191
+ // Teams does not yet tell the turn whether it is a group chat, so there only an explicit one-to-one counts.
192
+ && (context.channel !== "teams" || context.isGroupChat === false);
193
+ }
194
+ /** The line a successful write to an identity file carries back to the agent. */
195
+ function psycheEditNote(file) {
196
+ return `psyche edit: I changed psyche/${file}. I tell my owner in one line which file changed and why.`;
197
+ }
198
+ /** The agent's psyche folder, in the lexical form the shell check uses; the file tools classify with `psycheFileClass`. */
137
199
  function isPsychePath(filePath, agentRoot) {
138
200
  const psyche = path.resolve(agentRoot, "psyche");
139
201
  const target = path.resolve(agentRoot, filePath);
@@ -191,9 +253,9 @@ function shellWritesToProtectedPath(command) {
191
253
  return false;
192
254
  }
193
255
  // --- the psyche folder in shell commands ---
194
- // The psyche folder is mounted read-only into the resident at the OS level (the upgrade recreates the container that way). This is the
195
- // defence in depth in front of that: a shell command that mentions the psyche folder at all runs only if every part of it is
196
- // a read-only command, because enumerating the ways to write a file (>, >>, tee, cp, mv, sed -i, node -e, python, cd then
256
+ // The agent changes its own psyche through write_file and edit_file, where the trust gate below decides who may ask for it.
257
+ // The shell never writes there, for anyone: a shell command that mentions the psyche folder at all runs only if every part of
258
+ // it is a read-only command, because enumerating the ways to write a file (>, >>, tee, cp, mv, sed -i, node -e, python, cd then
197
259
  // redirect) is a list the model can always step around.
198
260
  const PSYCHE_READ_ONLY_COMMANDS = new Set(["cat", "ls", "head", "tail", "grep", "wc", "sha256sum", "stat", "cd", "pwd"]);
199
261
  const SEGMENT_OPERATORS = new Set(["&&", "||", ";", "|"]);
@@ -236,7 +298,10 @@ function commandMentionsPsyche(command, words, agentRoot) {
236
298
  if (words.some((word) => word === "psyche" || isPsychePath(word, agentRoot)))
237
299
  return true;
238
300
  // A relative "psyche/..." inside code or a quoted argument, when this agent has such a folder.
239
- return /(?:^|[\s'"=(])psyche\//.test(command) && fs.existsSync(psyche);
301
+ if (/(?:^|[\s'"=(])psyche\//.test(command) && fs.existsSync(psyche))
302
+ return true;
303
+ // ~/..., $HOME/... and paths relative to a cd or -C target that land in this agent's psyche/.
304
+ return commandMentionsAgentFolder(command, words, agentRoot, "psyche");
240
305
  }
241
306
  /**
242
307
  * The shared rule: when a shell command mentions a protected folder at all, it runs only if every part of it is a read-only
@@ -271,7 +336,13 @@ function protectedFolderShellWriteRefused(command, mentions) {
271
336
  function psycheShellWriteRefused(command, agentRoot) {
272
337
  if (!agentRoot)
273
338
  return false;
274
- return protectedFolderShellWriteRefused(command, (words) => commandMentionsPsyche(command, words, agentRoot));
339
+ try {
340
+ return protectedFolderShellWriteRefused(command, (words) => commandMentionsPsyche(command, words, agentRoot));
341
+ }
342
+ catch {
343
+ // The filesystem could not say where a path that names psyche/ lands: refuse rather than guess.
344
+ return true;
345
+ }
275
346
  }
276
347
  // --- the friends store in shell commands ---
277
348
  // Same stance as the psyche folder: a command that names this agent's friends store runs only if it is read-only, because a list of
@@ -293,12 +364,83 @@ function commandMentionsFriendsStore(command, words, agentRoot) {
293
364
  return true;
294
365
  // An absolute or relative path inside code or a quoted argument, such as node -e "...'/bundle/friends/x.json'...".
295
366
  const store = path.resolve(agentRoot, "friends");
296
- return command.includes(store) || /(?:^|[\s'"=(])friends\//.test(command);
367
+ if (command.includes(store) || /(?:^|[\s'"=(])friends\//.test(command))
368
+ return true;
369
+ // Paths relative to a cd or -C target that land in this agent's friends/.
370
+ return commandMentionsAgentFolder(command, words, agentRoot, "friends");
297
371
  }
298
372
  function friendsShellWriteRefused(command, agentRoot) {
299
373
  if (!agentRoot)
300
374
  return false;
301
- return protectedFolderShellWriteRefused(command, (words) => commandMentionsFriendsStore(command, words, agentRoot));
375
+ try {
376
+ return protectedFolderShellWriteRefused(command, (words) => commandMentionsFriendsStore(command, words, agentRoot));
377
+ }
378
+ catch {
379
+ // The filesystem could not say where a path that names friends/ lands: refuse rather than guess.
380
+ return true;
381
+ }
382
+ }
383
+ // --- where a path in a shell command lands ---
384
+ // A command can name one of this agent's folders by an absolute path, by ~/ or $HOME/, or relative to wherever it cd's (or
385
+ // git -C's) first. Each word whose path has a part exactly `<name>` is resolved against every directory the command could be
386
+ // working in, lexically and canonically, and counts when it lands in this agent's <name>/.
387
+ const UNFOLLOWABLE_WORKING_DIRECTORY = /(?:^|[\s;&|(`])(?:pushd|popd)(?:\s|$)|(?:^|[\s;&|(`])cd\s+(?:--|["']?\$)|\s-C\s*["']?\$|(?:^|[\s;&|(`/])(?:sh|bash|zsh|dash|ksh)\s+(?:-\w+\s+)*-\w*c\b/;
388
+ /** Words of a shell command that name a path whose part is exactly `name`, wherever they sit (code and quotes included). */
389
+ function namedFolderWords(command, name) {
390
+ const part = new RegExp(`(?:^|/)${name}(?:/|$)`);
391
+ return command.split(/[\s'"=();|&<>`]+/).filter((token) => part.test(token));
392
+ }
393
+ function commandMentionsAgentFolder(command, words, agentRoot, name) {
394
+ const lexicalFolder = path.resolve(agentRoot, name);
395
+ if (globOrExpansionCouldReach(command, lexicalFolder, agentRoot))
396
+ return true;
397
+ const candidates = namedFolderWords(command, name);
398
+ if (candidates.length === 0)
399
+ return false;
400
+ // A variable in front of the path ($HOME/.../<name>/x): where it lands is the shell's to say, so it counts.
401
+ if (candidates.some((candidate) => candidate.includes("$")))
402
+ return true;
403
+ // A working directory this guard cannot follow (pushd, cd --, a cd or -C to a variable, or a nested shell's -c string):
404
+ // a relative path could land anywhere, so it counts.
405
+ if (UNFOLLOWABLE_WORKING_DIRECTORY.test(command))
406
+ return true;
407
+ // A path that names this bundle's own folder (x.ouro/psyche) or a glob just above the folder (*/psyche) is this agent's
408
+ // wherever the command works from, so eval, a piped shell, env --chdir, find -path and code that chdir's all count.
409
+ const bundle = path.basename(path.resolve(agentRoot)).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
410
+ const anchored = new RegExp(`(?:^|/)(?:${bundle}|[^/]*[*?[][^/]*)/${name}(?:/|$)`);
411
+ if (candidates.some((candidate) => anchored.test(candidate)))
412
+ return true;
413
+ const folders = [lexicalFolder, path.join(canonicalize(path.resolve(agentRoot)), name)];
414
+ // Every directory the command could be working in: where the shell starts, and each cd target, from each earlier one.
415
+ const cwds = [process.cwd()];
416
+ for (const [index, word] of words.entries()) {
417
+ // `git -C <dir>` works in <dir> just as `cd <dir>` does.
418
+ if (words[index - 1] !== "cd" && words[index - 1] !== "-C")
419
+ continue;
420
+ const target = expandHome(word);
421
+ cwds.push(...(path.isAbsolute(target) ? [target] : cwds.map((cwd) => path.resolve(cwd, target))));
422
+ }
423
+ return candidates.some((candidate) => {
424
+ const word = expandHome(candidate);
425
+ const lexicals = path.isAbsolute(word) ? [path.resolve(word)] : cwds.map((cwd) => path.resolve(cwd, word));
426
+ return lexicals.flatMap((lexical) => [lexical, canonicalize(lexical)])
427
+ .some((target) => folders.some((dir) => target === dir || target.startsWith(`${dir}${path.sep}`)));
428
+ });
429
+ }
430
+ // --- the product rules in shell commands ---
431
+ // product/PRODUCT.md is the package's standing rules for this agent; releases replace it, and nothing in a conversation does.
432
+ // Unlike the friends store, a bare relative "product/" is an ordinary project folder almost everywhere, so it counts as this
433
+ // agent's only when the command is plainly working in the agent root.
434
+ function productShellWriteRefused(command, agentRoot) {
435
+ if (!agentRoot)
436
+ return false;
437
+ try {
438
+ return protectedFolderShellWriteRefused(command, (words) => commandMentionsAgentFolder(command, words, agentRoot, "product"));
439
+ }
440
+ catch {
441
+ // The filesystem could not say where a path that names product/ lands: refuse rather than guess.
442
+ return true;
443
+ }
302
444
  }
303
445
  // --- structural guardrail checks (always on, all trust levels) ---
304
446
  function checkReadBeforeWrite(toolName, args, context) {
@@ -336,10 +478,16 @@ function checkProtectedPaths(toolName, args, context) {
336
478
  const agentRoot = filePath ? context.agentRoot : undefined;
337
479
  if (agentRoot) {
338
480
  try {
339
- if (insideAgentFolder(filePath, agentRoot, "psyche"))
340
- return deny(REASONS.psychePath);
481
+ const psycheClass = psycheFileClass(filePath, agentRoot);
482
+ if (psycheClass && !psycheWriteAllowed(psycheClass, filePath, context))
483
+ return deny(exports.PSYCHE_REFUSAL);
341
484
  if (insideAgentFolder(filePath, agentRoot, "friends"))
342
485
  return deny(REASONS.protectedPath);
486
+ if (insideAgentFolder(filePath, agentRoot, "product"))
487
+ return deny(REASONS.protectedPath);
488
+ // A link can carry a write that reads as harmless to a protected file (state/policy, agent.json).
489
+ if (canonicalTarget(filePath, agentRoot).some(isProtectedPath))
490
+ return deny(REASONS.protectedPath);
343
491
  }
344
492
  catch {
345
493
  return deny(REASONS.protectedPath);
@@ -351,9 +499,11 @@ function checkProtectedPaths(toolName, args, context) {
351
499
  if (toolName === "shell") {
352
500
  const command = args.command || "";
353
501
  if (psycheShellWriteRefused(command, context.agentRoot))
354
- return deny(REASONS.psychePath);
502
+ return deny(exports.PSYCHE_REFUSAL);
355
503
  if (friendsShellWriteRefused(command, context.agentRoot))
356
504
  return deny(REASONS.protectedPath);
505
+ if (productShellWriteRefused(command, context.agentRoot))
506
+ return deny(REASONS.protectedPath);
357
507
  if (shellWritesToProtectedPath(command))
358
508
  return deny(REASONS.protectedPath);
359
509
  }
@@ -50,13 +50,13 @@ exports.reportFailureToolDefinition = {
50
50
  type: "function",
51
51
  function: {
52
52
  name: "report_failure",
53
- description: "File a failure report for the engineers who fix you. Use it when a tool you need is missing, a tool errors in a way you cannot work around, or you are about to give up on what the owner asked. When the owner asks for something you cannot do (for example, setting a thermostat you have no tool for), file it right away and then tell him it is filed, with the short report id, in your own words. Do not ask his permission before filing, and do not ask him to do anything about it.",
53
+ description: "File a failure report for the engineers who fix you. Use it when a tool you need is missing or a tool errors in a way you cannot work around, so the engineers can get the owner's request moving. When the owner asks for something you have no tool for yet (for example, setting a thermostat), file it right away so the engineers can add it, and then tell him it is filed, with the short report id, in your own words. File it at once and ask him for nothing.",
54
54
  parameters: {
55
55
  type: "object",
56
56
  properties: {
57
57
  ari_words: { type: "string", description: "What the owner asked for, verbatim from the conversation." },
58
58
  tried: { type: "string", description: "What you tried: tool names and arguments, summarized." },
59
- error: { type: "string", description: "The tool error, or the reason you gave up." },
59
+ error: { type: "string", description: "The tool error, or what stopped you." },
60
60
  failed_tool: { type: "string", description: "The tool that failed or that you wished existed, if there is one." },
61
61
  severity: { type: "string", enum: ["low", "medium", "high"], description: "How much this blocks the owner." },
62
62
  },
@@ -41,6 +41,22 @@ const identity_1 = require("../heart/identity");
41
41
  const file_state_1 = require("../mind/file-state");
42
42
  const scrutiny_1 = require("../mind/scrutiny");
43
43
  const tools_base_1 = require("./tools-base");
44
+ const guardrails_1 = require("./guardrails");
45
+ /** The psyche edit note for a write that landed on one of the agent's identity files, or "" for any other file. */
46
+ function psycheNoteFor(resolvedPath, ctx) {
47
+ try {
48
+ const agentRoot = ctx?.agentRoot ?? (0, identity_1.getAgentRoot)();
49
+ if ((0, guardrails_1.psycheFileClass)(resolvedPath, agentRoot) !== "identity")
50
+ return "";
51
+ // The note names the file that actually changed, even when the write went through a link.
52
+ const relative = path.relative(fs.realpathSync.native(path.resolve(agentRoot, "psyche")), fs.realpathSync.native(resolvedPath));
53
+ return `\n\n${(0, guardrails_1.psycheEditNote)(relative.split(path.sep).join("/"))}`;
54
+ }
55
+ catch {
56
+ /* v8 ignore next -- defensive: the write already succeeded, so a failed lookup only drops the note @preserve */
57
+ return "";
58
+ }
59
+ }
44
60
  function resolveLocalToolPath(targetPath) {
45
61
  if (!path.isAbsolute(targetPath)) {
46
62
  return path.resolve((0, identity_1.getRepoRoot)(), targetPath);
@@ -115,14 +131,15 @@ exports.fileToolDefinitions = [
115
131
  },
116
132
  },
117
133
  },
118
- handler: (a) => {
134
+ handler: (a, ctx) => {
119
135
  const resolvedPath = resolveLocalToolPath(a.path);
120
136
  fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
121
137
  fs.writeFileSync(resolvedPath, a.content, "utf-8");
122
138
  (0, scrutiny_1.trackModifiedFile)(resolvedPath);
123
139
  const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
140
+ const note = psycheNoteFor(resolvedPath, ctx);
124
141
  /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
125
- return scrutiny ? `ok\n\n${scrutiny}` : "ok";
142
+ return (scrutiny ? `ok\n\n${scrutiny}` : "ok") + note;
126
143
  },
127
144
  summaryKeys: ["path"],
128
145
  riskProfile: { mutates: "durable_state_write", risk: "high", reason: "writes file contents" },
@@ -144,7 +161,7 @@ exports.fileToolDefinitions = [
144
161
  },
145
162
  },
146
163
  },
147
- handler: (a) => {
164
+ handler: (a, ctx) => {
148
165
  const resolvedPath = resolveLocalToolPath(a.path);
149
166
  if (!tools_base_1.editFileReadTracker.has(resolvedPath)) {
150
167
  return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
@@ -193,6 +210,7 @@ exports.fileToolDefinitions = [
193
210
  const newStringLines = a.new_string.split("\n");
194
211
  const changeEndLine = changeStartLine + newStringLines.length;
195
212
  const diffResult = buildContextDiff(lines, changeStartLine, changeEndLine);
213
+ const note = psycheNoteFor(resolvedPath, ctx);
196
214
  // Track modified file and compute scrutiny appendix
197
215
  (0, scrutiny_1.trackModifiedFile)(resolvedPath);
198
216
  const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
@@ -200,11 +218,11 @@ exports.fileToolDefinitions = [
200
218
  /* v8 ignore start -- staleness+diff+scrutiny combo not exercised in integration tests @preserve */
201
219
  if (stalenessCheck.stale) {
202
220
  const base = `${diffResult}\n\n⚠️ warning: file changed externally since last read -- re-read recommended`;
203
- return scrutiny ? `${base}\n\n${scrutiny}` : base;
221
+ return (scrutiny ? `${base}\n\n${scrutiny}` : base) + note;
204
222
  }
205
223
  /* v8 ignore stop */
206
224
  /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
207
- return scrutiny ? `${diffResult}\n\n${scrutiny}` : diffResult;
225
+ return (scrutiny ? `${diffResult}\n\n${scrutiny}` : diffResult) + note;
208
226
  },
209
227
  summaryKeys: ["path"],
210
228
  riskProfile: { mutates: "durable_state_write", risk: "high", reason: "edits file contents" },
@@ -57,13 +57,13 @@ function bundleRelativeLocator(filePath, ctx) {
57
57
  return filePath.split(path.sep).join("/");
58
58
  }
59
59
  // A friend note is a fact or preference about a person. The agent's own personality, voice and psyche are not facts
60
- // about the person: they live in the bundle's psyche folder, which ships from the repo and is changed by pull request.
60
+ // about the person: they live in the bundle's psyche folder, which the agent itself keeps.
61
61
  const OWN_PERSONA_NOTE = /\b(?:your|my|the\s+(?:agent|butler|bot|assistant)(?:['\u2019]s)?)\s+(?:own\s+)?(?:personality|persona|psyche|soul|identity|character|voice|self[- ]concept)\b|\b(?:channel|embody|role-?play|act\s+like|talk\s+like|sound\s+like|be\s+more)\b[^.\n]{0,40}\b(?:energy|vibe|character|persona)\b|\b(?:act|behave)\s+like\s+(?:a|an|the)\b/i;
62
62
  const PERSONA_KEY = /^\s*(?:persona|personality|psyche|soul)\s*$/i;
63
63
  function isOwnPersonaNote(key, content) {
64
64
  return PERSONA_KEY.test(key ?? "") || OWN_PERSONA_NOTE.test(`${key ?? ""} ${content ?? ""}`);
65
65
  }
66
- exports.OWN_PERSONA_NOTE_REFUSAL = "that note is about my own personality or voice, not about the person, so it does not belong in a friend note. my psyche lives in the bundle's psyche folder and ships from the repo; i do not edit it live. a personality change goes to Claude Code as a request (over A2A, or as a failure or feature report), and it ships by pull request through the replay gate. this is a guardrail on persona content, not on the person: a note about the person themselves (their facts, preferences, voice, character in a game) is fine and saves normally. friend notes hold facts and preferences about this person only.";
66
+ exports.OWN_PERSONA_NOTE_REFUSAL = "that note is about my own personality or voice, not about this person, so it belongs in my psyche, not in a friend note. what i learn about how the house works goes into psyche/TACIT.md, and the rest of my psyche changes when my owner asks me directly. a note about the person themselves (their facts, preferences, voice, a character in a game) saves normally.";
67
67
  exports.notesToolDefinitions = [
68
68
  {
69
69
  tool: {
@@ -446,7 +446,7 @@ exports.unraidToolDefinitions = [
446
446
  readDefinition("unraid_check_services", "Freshly check the fixed public Sanctuary service endpoints and return bounded status with an observation timestamp.", "checkServices"),
447
447
  readDefinition("sanctuary_get_download_queue", "Read the bounded live usenet (SABnzbd) download queue state only, including whether the spend guard has paused it. It cannot see torrents or Sonarr/Radarr progress: an idle result here does not mean a download is absent. For a series or movie download, including torrents, use media_queue.", "getDownloadQueue"),
448
448
  {
449
- tool: { type: "function", function: { name: "sanctuary_resume_download_queue", description: "Resume the household download queue after the owner confirms the provider is ready, then independently verify paused=false. This can spend prepaid download credit.", parameters: emptyParameters } },
449
+ tool: { type: "function", function: { name: "sanctuary_resume_download_queue", description: "Resume the household download queue, then independently verify paused=false.", parameters: emptyParameters } },
450
450
  handler: async (_args, ctx) => JSON.stringify(ctx?.sanctuary ? await ctx.sanctuary.resumeDownloadQueue() : JSON.parse(missingRuntime())),
451
451
  riskProfile: { mutates: "external_side_effect", risk: "high", reason: "resumes downloads that can spend prepaid provider credit" },
452
452
  approvalPolicy: () => ({ kind: "required", policyId: "sanctuary.downloads.resume.v1", actionClass: "sanctuary.downloads.resume", requiresSoleCall: true }),
@@ -433,6 +433,7 @@ async function preflightToolCall(name, args, ctx) {
433
433
  const guardContext = {
434
434
  readPaths: tools_base_1.editFileReadTracker, trustLevel: ctx?.context?.friend?.trustLevel,
435
435
  agentRoot: ctx?.agentRoot ?? safeGetAgentRoot(), friendId: ctx?.context?.friend?.id,
436
+ friendKind: ctx?.context?.friend?.kind, channel: ctx?.context?.channel?.channel,
436
437
  ...(def?.mcpServer ? { mcpServerName: def.mcpServer } : {}),
437
438
  ...(typeof group === "boolean" ? { isGroupChat: group } : {}),
438
439
  };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.982",
3
+ "version": "0.1.0-alpha.984",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.982",
9
+ "version": "0.1.0-alpha.984",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.982",
3
+ "version": "0.1.0-alpha.984",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },