@ouro.bot/cli 0.1.0-alpha.62 → 0.1.0-alpha.65

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 CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.65",
6
+ "changes": [
7
+ "Tool permissions overhauled: channel-level blocking removed, all tools now visible on all channels. Guardrails are invocation-level with two layers \u2014 structural (edit-requires-read, destructive pattern blocking, protected paths) always on for everyone, and trust-level (ouro CLI per-subcommand trust manifest, general CLI allowlists, bundle-scoped writes) for untrusted contexts.",
8
+ "New `ouro changelog` CLI subcommand reads changelog.json and supports `--from <version>` for delta filtering, so agents can introspect their own update history on any channel.",
9
+ "Compound shell commands (&&, ;, |, $()) are blocked for untrusted users to prevent smuggling dangerous operations behind safe prefixes.",
10
+ "Azure App Service deployment migrated from zip-deploy to npm-based harness install with persistent agent bundle and managed identity auth."
11
+ ]
12
+ },
13
+ {
14
+ "version": "0.1.0-alpha.63",
15
+ "changes": [
16
+ "ensureSkillManagement now iterates all discovered agent bundles instead of requiring a single-agent context. Fixes skill-management not being installed during ouro up."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.62",
6
21
  "changes": [
@@ -136,9 +151,9 @@
136
151
  "changes": [
137
152
  "Inner dialog now knows which task triggered it: taskId flows from daemon poke through the worker into the turn, and the agent gets the full task file content instead of a generic heartbeat prompt.",
138
153
  "Inner dialog boot message includes aspirations and state summary instead of a vacuous placeholder, so the agent wakes up with context about what matters and what's happening.",
139
- "Vestigial `drainInbox` removed from inner dialog pipeline already handles pending drain correctly.",
154
+ "Vestigial `drainInbox` removed from inner dialog \u2014 pipeline already handles pending drain correctly.",
140
155
  "Inner dialog nerves events now include assistant response preview, tool call names, token usage, and taskId for meaningful observability.",
141
- "`ouro thoughts` command reads and formats inner dialog session turns with `--last`, `--json`, `--follow`, and `--agent` flags humans can now see what the agent has been thinking.",
156
+ "`ouro thoughts` command reads and formats inner dialog session turns with `--last`, `--json`, `--follow`, and `--agent` flags \u2014 humans can now see what the agent has been thinking.",
142
157
  "`readTaskFile` searches collection subdirectories (one-shots, ongoing, habits) since the scheduler sends bare task stems without collection prefixes.",
143
158
  "`ouro reminder create` accepts `--requester` to track who requested a reminder for notification round-trip.",
144
159
  "Response extraction handles `tool_choice=required` models by falling back to `final_answer` tool call arguments when assistant message content is empty."
@@ -168,25 +183,25 @@
168
183
  {
169
184
  "version": "0.1.0-alpha.42",
170
185
  "changes": [
171
- "Associative recall now skips corrupt JSONL lines instead of crashing matches the resilient pattern already used in memory.ts."
186
+ "Associative recall now skips corrupt JSONL lines instead of crashing \u2014 matches the resilient pattern already used in memory.ts."
172
187
  ]
173
188
  },
174
189
  {
175
190
  "version": "0.1.0-alpha.41",
176
191
  "changes": [
177
- "JSONL readers (memory facts, inter-agent inbox) now skip corrupt lines instead of crashing partial writes from crashes no longer lose all data.",
192
+ "JSONL readers (memory facts, inter-agent inbox) now skip corrupt lines instead of crashing \u2014 partial writes from crashes no longer lose all data.",
178
193
  "Inter-agent message router now parses before clearing the inbox file, and preserves unparsed lines so corrupt messages are not silently lost.",
179
- "Inner-dialog checkpoint derivation no longer crashes on all-whitespace assistant content returns fallback checkpoint instead.",
194
+ "Inner-dialog checkpoint derivation no longer crashes on all-whitespace assistant content \u2014 returns fallback checkpoint instead.",
180
195
  "Update checker interval now catches and logs errors from the onUpdate callback instead of silently swallowing them."
181
196
  ]
182
197
  },
183
198
  {
184
199
  "version": "0.1.0-alpha.40",
185
200
  "changes": [
186
- "Removed dead backward-compat re-exports from core.ts (tools, streaming, prompt, kicks) consumers already import from the canonical modules.",
201
+ "Removed dead backward-compat re-exports from core.ts (tools, streaming, prompt, kicks) \u2014 consumers already import from the canonical modules.",
187
202
  "Removed dead exports: baseToolHandlers, teamsToolHandlers, teamsTools, __internal (token-estimate), TASK_STEM_PATTERN, checkAndRecord403 no-op and METHOD_TO_ACTION.",
188
203
  "Consolidated duplicate sanitizeKey (config.ts + bluebubbles-mutation-log.ts) and slugify (hatch-flow.ts + tasks/index.ts) into shared exports from config.ts.",
189
- "Replaced all as-any casts in source with proper TypeScript narrowing or Record<string, unknown> only 2 SDK-required casts remain.",
204
+ "Replaced all as-any casts in source with proper TypeScript narrowing or Record<string, unknown> \u2014 only 2 SDK-required casts remain.",
190
205
  "Removed unnecessary as-unknown-as casts on readdirSync (4 locations) and spawner double-cast.",
191
206
  "Cleaned up commented-out kick detection code, stale TODOs, misplaced imports, and unused type imports."
192
207
  ]
@@ -194,9 +209,9 @@
194
209
  {
195
210
  "version": "0.1.0-alpha.39",
196
211
  "changes": [
197
- "All senses now route through a shared per-turn pipeline friend resolution, trust gate, session load, pending drain, agent turn, post-turn, and token accumulation happen in one place instead of four.",
212
+ "All senses now route through a shared per-turn pipeline \u2014 friend resolution, trust gate, session load, pending drain, agent turn, post-turn, and token accumulation happen in one place instead of four.",
198
213
  "Trust gate is now channel-aware: open senses (iMessage) enforce stranger/acquaintance rules, closed senses (Teams) trust the org, local and internal always pass through.",
199
- "Tool access and prompt restrictions use a single shared isTrustedLevel check no more scattered family/friend comparisons that could drift apart.",
214
+ "Tool access and prompt restrictions use a single shared isTrustedLevel check \u2014 no more scattered family/friend comparisons that could drift apart.",
200
215
  "Pending messages now inject correctly into multimodal content (image attachments no longer silently drop pending messages).",
201
216
  "ouro reminder create supports --agent flag, matching every other identity-scoped CLI command."
202
217
  ]
@@ -204,11 +219,11 @@
204
219
  {
205
220
  "version": "0.1.0-alpha.38",
206
221
  "changes": [
207
- "You now have a proper body map understanding of your home (bundle) and bones (harness), what each directory is for, and how to modify your own configuration.",
222
+ "You now have a proper body map \u2014 understanding of your home (bundle) and bones (harness), what each directory is for, and how to modify your own configuration.",
208
223
  "Inner dialog is now genuine internal monologue with metacognitive framing, not a second CLI session. Heartbeat and bootstrap messages read as first-person awareness.",
209
224
  "Cross-session communication works end-to-end: inner dialog thoughts surface as [inner thought: ...] in conversations, messages to yourself route to inner dialog, and you can proactively reach out to friends via iMessage and Teams.",
210
225
  "Tool audit: removed wrapper tools (git_commit, gh_cli, get_current_time, list_directory), added surgical tools (edit_file, glob, grep, read_file with offset/limit), consolidated 7 task tools + schedule_reminder + friend tools into ouro CLI commands.",
211
- "You now understand why certain tools are restricted in certain contexts trust level and shared channels each have independent, explained gates.",
226
+ "You now understand why certain tools are restricted in certain contexts \u2014 trust level and shared channels each have independent, explained gates.",
212
227
  "ouro friend link/unlink commands handle orphan cleanup when linking external identities, merging duplicate friend records intelligently.",
213
228
  "During onboarding, the adoption specialist can collect phone number and Teams handle to create an initial friend record with contact info."
214
229
  ]
@@ -703,6 +703,16 @@ function parseOuroCommand(args) {
703
703
  }
704
704
  if (head === "session")
705
705
  return parseSessionCommand(args.slice(1));
706
+ if (head === "changelog") {
707
+ const sliced = args.slice(1);
708
+ const { agent, rest: remaining } = extractAgentFlag(sliced);
709
+ let from;
710
+ const fromIdx = remaining.indexOf("--from");
711
+ if (fromIdx !== -1 && remaining[fromIdx + 1]) {
712
+ from = remaining[fromIdx + 1];
713
+ }
714
+ return { kind: "changelog", ...(from ? { from } : {}), ...(agent ? { agent } : {}) };
715
+ }
706
716
  if (head === "thoughts")
707
717
  return parseThoughtsCommand(args.slice(1));
708
718
  if (head === "chat") {
@@ -1178,6 +1188,7 @@ async function performSystemSetup(deps) {
1178
1188
  if (deps.ensureSkillManagement) {
1179
1189
  try {
1180
1190
  await deps.ensureSkillManagement();
1191
+ /* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
1181
1192
  }
1182
1193
  catch (error) {
1183
1194
  (0, runtime_1.emitNervesEvent)({
@@ -1185,9 +1196,10 @@ async function performSystemSetup(deps) {
1185
1196
  component: "daemon",
1186
1197
  event: "daemon.system_setup_skill_management_error",
1187
1198
  message: "failed to ensure skill-management skill",
1188
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1199
+ meta: { error: error instanceof Error ? error.message : String(error) },
1189
1200
  });
1190
1201
  }
1202
+ /* v8 ignore stop */
1191
1203
  }
1192
1204
  // Register .ouro bundle type (UTI on macOS)
1193
1205
  await registerOuroBundleTypeNonBlocking(deps);
@@ -1571,6 +1583,44 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1571
1583
  }
1572
1584
  /* v8 ignore stop */
1573
1585
  }
1586
+ // ── changelog (local, no daemon socket needed) ──
1587
+ if (command.kind === "changelog") {
1588
+ try {
1589
+ const changelogPath = deps.getChangelogPath
1590
+ ? deps.getChangelogPath()
1591
+ : (0, bundle_manifest_1.getChangelogPath)();
1592
+ const raw = fs.readFileSync(changelogPath, "utf-8");
1593
+ const entries = JSON.parse(raw);
1594
+ let filtered = entries;
1595
+ if (command.from) {
1596
+ const fromVersion = command.from;
1597
+ filtered = entries.filter((e) => e.version > fromVersion);
1598
+ }
1599
+ if (filtered.length === 0) {
1600
+ const message = "no changelog entries found.";
1601
+ deps.writeStdout(message);
1602
+ return message;
1603
+ }
1604
+ const lines = [];
1605
+ for (const entry of filtered) {
1606
+ lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
1607
+ if (entry.changes) {
1608
+ for (const change of entry.changes) {
1609
+ lines.push(`- ${change}`);
1610
+ }
1611
+ }
1612
+ lines.push("");
1613
+ }
1614
+ const message = lines.join("\n").trim();
1615
+ deps.writeStdout(message);
1616
+ return message;
1617
+ }
1618
+ catch {
1619
+ const message = "no changelog entries found.";
1620
+ deps.writeStdout(message);
1621
+ return message;
1622
+ }
1623
+ }
1574
1624
  // ── thoughts (local, no daemon socket needed) ──
1575
1625
  if (command.kind === "thoughts") {
1576
1626
  try {
@@ -36,15 +36,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ensureSkillManagement = ensureSkillManagement;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const identity_1 = require("../identity");
40
39
  const runtime_1 = require("../../nerves/runtime");
40
+ const identity_1 = require("../identity");
41
41
  const SKILL_MANAGEMENT_URL = "https://raw.githubusercontent.com/ouroborosbot/ouroboros-skills/main/skills/skill-management/SKILL.md";
42
42
  async function ensureSkillManagement() {
43
- const skillsDir = path.join((0, identity_1.getAgentRoot)(), "skills");
44
- const targetPath = path.join(skillsDir, "skill-management.md");
45
- if (fs.existsSync(targetPath)) {
43
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
44
+ if (!fs.existsSync(bundlesRoot))
45
+ return;
46
+ // Find all agent bundles
47
+ const entries = fs.readdirSync(bundlesRoot).filter(e => e.endsWith(".ouro"));
48
+ if (entries.length === 0)
49
+ return;
50
+ // Check if ANY bundle is missing the skill
51
+ const missing = entries.filter(e => {
52
+ const targetPath = path.join(bundlesRoot, e, "skills", "skill-management.md");
53
+ return !fs.existsSync(targetPath);
54
+ });
55
+ if (missing.length === 0)
46
56
  return;
47
- }
48
57
  // eslint-disable-next-line no-console -- terminal UX: visible install status
49
58
  console.log("installing skill-management from ouroboros-skills...");
50
59
  try {
@@ -62,10 +71,14 @@ async function ensureSkillManagement() {
62
71
  return;
63
72
  }
64
73
  const content = await response.text();
65
- fs.mkdirSync(skillsDir, { recursive: true });
66
- fs.writeFileSync(targetPath, content, "utf-8");
74
+ for (const bundle of missing) {
75
+ const skillsDir = path.join(bundlesRoot, bundle, "skills");
76
+ const targetPath = path.join(skillsDir, "skill-management.md");
77
+ fs.mkdirSync(skillsDir, { recursive: true });
78
+ fs.writeFileSync(targetPath, content, "utf-8");
79
+ }
67
80
  // eslint-disable-next-line no-console -- terminal UX: visible install status
68
- console.log("✓ installed skill-management");
81
+ console.log(`✓ installed skill-management (${missing.length} agent${missing.length > 1 ? "s" : ""})`);
69
82
  }
70
83
  catch (error) {
71
84
  // eslint-disable-next-line no-console -- terminal UX: visible install status
@@ -320,15 +320,31 @@ function toolsSection(channel, options, context) {
320
320
  return `## my tools\n${list}`;
321
321
  }
322
322
  function toolRestrictionSection(context) {
323
- if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
324
- return "";
325
- if ((0, types_1.isTrustedLevel)(context.friend.trustLevel))
326
- return "";
327
- const toolList = [...tools_1.REMOTE_BLOCKED_LOCAL_TOOLS].join(", ");
328
- return `## restricted tools
329
- some of my tools are unavailable right now: ${toolList}
330
-
331
- i don't know this person well enough yet to run local operations on their behalf. i can suggest remote-safe alternatives or ask them to run it from CLI.`;
323
+ const lines = [];
324
+ // Structural guardrails apply to everyone, every channel
325
+ lines.push(`## tool guardrails`);
326
+ lines.push(`i always read a file before editing or overwriting it.`);
327
+ lines.push(`certain paths (.git, secrets) are protected from writes.`);
328
+ lines.push(`destructive shell commands (rm -rf /, etc.) are always blocked.`);
329
+ // Trust-level guardrails only relevant for untrusted on remote channels
330
+ if (context?.friend && (0, channel_1.isRemoteChannel)(context.channel) && !(0, types_1.isTrustedLevel)(context.friend.trustLevel)) {
331
+ lines.push(``);
332
+ lines.push(`some operations are guardrailed based on how well i know someone.`);
333
+ lines.push(`if something i try is blocked, i get a clear reason — i relay it warmly, not as a policy error.`);
334
+ lines.push(``);
335
+ lines.push(`what's always open:`);
336
+ lines.push(`- read-only operations (reading files, searching, exploring)`);
337
+ lines.push(`- ouro self-introspection (whoami, changelog, session list)`);
338
+ lines.push(``);
339
+ lines.push(`what needs a closer relationship:`);
340
+ lines.push(`- writing or editing files outside my home`);
341
+ lines.push(`- shell commands that modify things or access the network`);
342
+ lines.push(`- ouro commands that touch personal data (friend list, task board)`);
343
+ lines.push(`- compound shell commands (&&, ;, |)`);
344
+ lines.push(``);
345
+ lines.push(`i adjust naturally based on trust — no need to explain the system unless asked.`);
346
+ }
347
+ return lines.join("\n");
332
348
  }
333
349
  function trustContextSection(context) {
334
350
  if (!context?.friend)
@@ -0,0 +1,274 @@
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.OURO_CLI_TRUST_MANIFEST = void 0;
37
+ exports.guardInvocation = guardInvocation;
38
+ const fs = __importStar(require("node:fs"));
39
+ const os = __importStar(require("node:os"));
40
+ const types_1 = require("../mind/friends/types");
41
+ const runtime_1 = require("../nerves/runtime");
42
+ const deny = (reason) => ({ allowed: false, reason });
43
+ const allow = { allowed: true };
44
+ // --- reason templates ---
45
+ // Structural reasons (always-on, apply to everyone)
46
+ const REASONS = {
47
+ readBeforeEdit: "i need to read that file first before i can edit it.",
48
+ readBeforeOverwrite: "i need to read that file first before i can overwrite it.",
49
+ protectedPath: "that path is protected — i can read it but not modify it.",
50
+ destructiveCommand: "that command is too dangerous to run — it could cause irreversible damage.",
51
+ compoundCommand: "i can only run simple commands for you — no chaining with && or ;",
52
+ // Trust reasons (vary by relationship)
53
+ needsTrust: "i'd need a closer friend to vouch for you before i can do that.",
54
+ needsTrustForWrite: "i'd need a closer friend to vouch for you before i can write files outside my home.",
55
+ };
56
+ // --- read-only tools that never need guardrails ---
57
+ const READ_ONLY_TOOLS = new Set(["read_file", "glob", "grep"]);
58
+ // --- protected path detection ---
59
+ const PROTECTED_PATH_SEGMENTS = [".git/"];
60
+ function getProtectedAbsolutePrefixes() {
61
+ return [`${os.homedir()}/.agentsecrets/`];
62
+ }
63
+ function isProtectedPath(filePath) {
64
+ for (const segment of PROTECTED_PATH_SEGMENTS) {
65
+ if (filePath.includes(`/${segment}`) || filePath.startsWith(segment))
66
+ return true;
67
+ }
68
+ for (const prefix of getProtectedAbsolutePrefixes()) {
69
+ if (filePath.startsWith(prefix))
70
+ return true;
71
+ }
72
+ return false;
73
+ }
74
+ // --- destructive shell patterns ---
75
+ const DESTRUCTIVE_PATTERNS = [
76
+ /\brm\s+(-\w*\s+)*-\w*r\w*\s+(-\w+\s+)*[/~]/, // rm -rf / or rm -rf ~
77
+ /\bchmod\s+(-\w*\s+)*-\w*R\w*\s+\d+\s+\//, // chmod -R 777 /
78
+ /\bmkfs\b/, // mkfs.*
79
+ /\bdd\s+if=/, // dd if=
80
+ ];
81
+ function isDestructiveShellCommand(command) {
82
+ return DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
83
+ }
84
+ // --- compound command splitting ---
85
+ // Shell operators that chain commands: &&, ||, ;, |, $(), backticks
86
+ const COMPOUND_SEPARATORS = /\s*(?:&&|\|\||;|\|)\s*/;
87
+ const SUBSHELL_PATTERN = /\$\(|`/;
88
+ function splitShellCommands(command) {
89
+ if (SUBSHELL_PATTERN.test(command))
90
+ return [command];
91
+ return command.split(COMPOUND_SEPARATORS).filter(Boolean);
92
+ }
93
+ function isCompoundCommand(command) {
94
+ return SUBSHELL_PATTERN.test(command) || splitShellCommands(command).length > 1;
95
+ }
96
+ // --- shell commands that write to protected paths ---
97
+ function shellWritesToProtectedPath(command) {
98
+ const redirectMatch = command.match(/>\s*(\S+)/);
99
+ if (redirectMatch && isProtectedPath(redirectMatch[1]))
100
+ return true;
101
+ const teeMatch = command.match(/tee\s+(?:-\w+\s+)*(\S+)/);
102
+ if (teeMatch && isProtectedPath(teeMatch[1]))
103
+ return true;
104
+ return false;
105
+ }
106
+ // --- structural guardrail checks (always on, all trust levels) ---
107
+ function checkReadBeforeWrite(toolName, args, context) {
108
+ if (toolName === "edit_file") {
109
+ const filePath = args.path || "";
110
+ if (!context.readPaths.has(filePath))
111
+ return deny(REASONS.readBeforeEdit);
112
+ }
113
+ if (toolName === "write_file") {
114
+ const filePath = args.path || "";
115
+ if (context.readPaths.has(filePath))
116
+ return allow;
117
+ if (!fs.existsSync(filePath))
118
+ return allow;
119
+ return deny(REASONS.readBeforeOverwrite);
120
+ }
121
+ return allow;
122
+ }
123
+ function checkDestructiveShellPatterns(toolName, args) {
124
+ if (toolName !== "shell")
125
+ return allow;
126
+ const command = args.command || "";
127
+ // Check each subcommand in compound commands for destructive patterns
128
+ for (const sub of splitShellCommands(command)) {
129
+ if (isDestructiveShellCommand(sub))
130
+ return deny(REASONS.destructiveCommand);
131
+ }
132
+ return allow;
133
+ }
134
+ function checkProtectedPaths(toolName, args) {
135
+ if (toolName === "write_file" || toolName === "edit_file") {
136
+ const filePath = args.path || "";
137
+ if (isProtectedPath(filePath))
138
+ return deny(REASONS.protectedPath);
139
+ }
140
+ if (toolName === "shell") {
141
+ const command = args.command || "";
142
+ if (shellWritesToProtectedPath(command))
143
+ return deny(REASONS.protectedPath);
144
+ }
145
+ return allow;
146
+ }
147
+ function checkStructuralGuardrails(toolName, args, context) {
148
+ const protectedResult = checkProtectedPaths(toolName, args);
149
+ if (!protectedResult.allowed)
150
+ return protectedResult;
151
+ const destructiveResult = checkDestructiveShellPatterns(toolName, args);
152
+ if (!destructiveResult.allowed)
153
+ return destructiveResult;
154
+ return checkReadBeforeWrite(toolName, args, context);
155
+ }
156
+ // --- ouro CLI trust manifest ---
157
+ /** Minimum trust level required for each ouro CLI subcommand. */
158
+ exports.OURO_CLI_TRUST_MANIFEST = {
159
+ whoami: "acquaintance",
160
+ changelog: "acquaintance",
161
+ "session list": "acquaintance",
162
+ "task board": "friend",
163
+ "task create": "friend",
164
+ "task update": "friend",
165
+ "task show": "friend",
166
+ "task actionable": "friend",
167
+ "task deps": "friend",
168
+ "task sessions": "friend",
169
+ "friend list": "friend",
170
+ "friend show": "friend",
171
+ "friend create": "friend",
172
+ "reminder create": "friend",
173
+ };
174
+ // --- trust level comparison ---
175
+ const LEVEL_ORDER = {
176
+ stranger: 0,
177
+ acquaintance: 1,
178
+ friend: 2,
179
+ family: 3,
180
+ };
181
+ function trustLevelSatisfied(required, actual) {
182
+ return LEVEL_ORDER[actual] >= LEVEL_ORDER[required];
183
+ }
184
+ // --- general CLI allowlists for acquaintance ---
185
+ const ACQUAINTANCE_SHELL_ALLOWLIST = new Set([
186
+ "cat", "ls", "head", "tail", "wc", "file", "stat", "which", "echo",
187
+ "pwd", "env", "printenv", "whoami", "date", "uname",
188
+ ]);
189
+ const ACQUAINTANCE_GIT_ALLOWLIST = new Set([
190
+ "status", "log", "show", "diff", "branch",
191
+ ]);
192
+ // --- trust-level shell guardrails ---
193
+ function resolveOuroSubcommand(command) {
194
+ const afterOuro = command.replace(/^ouro\s+/, "").trim();
195
+ /* v8 ignore next -- bare "ouro" is caught upstream by checkShellTrustGuardrails @preserve */
196
+ if (!afterOuro)
197
+ return null;
198
+ const tokens = afterOuro.split(/\s+/);
199
+ const twoWord = tokens.length >= 2 ? `${tokens[0]} ${tokens[1]}` : null;
200
+ // Two-word match first (e.g. "task board"), then one-word (e.g. "whoami")
201
+ if (twoWord && exports.OURO_CLI_TRUST_MANIFEST[twoWord])
202
+ return twoWord;
203
+ if (exports.OURO_CLI_TRUST_MANIFEST[tokens[0]])
204
+ return tokens[0];
205
+ return null;
206
+ }
207
+ function checkSingleShellCommandTrust(command, trustLevel) {
208
+ const trimmed = command.trim();
209
+ const tokens = trimmed.split(/\s+/);
210
+ const firstToken = tokens[0] || "";
211
+ // ouro CLI — check per-subcommand trust manifest
212
+ if (firstToken === "ouro") {
213
+ const subcommand = resolveOuroSubcommand(trimmed);
214
+ const requiredLevel = subcommand ? exports.OURO_CLI_TRUST_MANIFEST[subcommand] : "friend";
215
+ if (trustLevelSatisfied(requiredLevel, trustLevel))
216
+ return allow;
217
+ return deny(REASONS.needsTrust);
218
+ }
219
+ // git — check subcommand allowlist
220
+ if (firstToken === "git") {
221
+ const gitSub = tokens[1] || "";
222
+ if (ACQUAINTANCE_GIT_ALLOWLIST.has(gitSub))
223
+ return allow;
224
+ return deny(REASONS.needsTrust);
225
+ }
226
+ // General CLI — check allowlist
227
+ if (ACQUAINTANCE_SHELL_ALLOWLIST.has(firstToken))
228
+ return allow;
229
+ return deny(REASONS.needsTrust);
230
+ }
231
+ function checkShellTrustGuardrails(command, trustLevel) {
232
+ // Compound commands: for untrusted users, reject entirely.
233
+ // This prevents "ouro whoami && rm -rf /" from smuggling dangerous commands.
234
+ if (isCompoundCommand(command))
235
+ return deny(REASONS.compoundCommand);
236
+ return checkSingleShellCommandTrust(command, trustLevel);
237
+ }
238
+ function checkWriteTrustGuardrails(toolName, args, context) {
239
+ if (toolName !== "write_file" && toolName !== "edit_file")
240
+ return allow;
241
+ const filePath = args.path || "";
242
+ if (context.agentRoot && filePath.startsWith(context.agentRoot))
243
+ return allow;
244
+ if (!context.agentRoot)
245
+ return allow;
246
+ return deny(REASONS.needsTrustForWrite);
247
+ }
248
+ function checkTrustLevelGuardrails(toolName, args, context) {
249
+ // Trusted levels (family/friend) — no trust guardrails. Undefined defaults to friend.
250
+ if ((0, types_1.isTrustedLevel)(context.trustLevel))
251
+ return allow;
252
+ if (toolName === "shell") {
253
+ return checkShellTrustGuardrails(args.command || "", context.trustLevel);
254
+ }
255
+ return checkWriteTrustGuardrails(toolName, args, context);
256
+ }
257
+ // --- main entry point ---
258
+ function guardInvocation(toolName, args, context) {
259
+ (0, runtime_1.emitNervesEvent)({
260
+ component: "tools",
261
+ event: "tools.guard_check",
262
+ message: "guardrail check",
263
+ meta: { toolName },
264
+ });
265
+ // Read-only tools are always allowed (no structural or trust guardrails)
266
+ if (READ_ONLY_TOOLS.has(toolName))
267
+ return allow;
268
+ // Layer 1: structural guardrails (always on)
269
+ const structuralResult = checkStructuralGuardrails(toolName, args, context);
270
+ if (!structuralResult.allowed)
271
+ return structuralResult;
272
+ // Layer 2: trust-level guardrails (varies by friend's trust)
273
+ return checkTrustLevelGuardrails(toolName, args, context);
274
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.REMOTE_BLOCKED_LOCAL_TOOLS = exports.finalAnswerTool = exports.tools = void 0;
3
+ exports.finalAnswerTool = exports.tools = void 0;
4
4
  exports.getToolsForChannel = getToolsForChannel;
5
5
  exports.isConfirmationRequired = isConfirmationRequired;
6
6
  exports.execTool = execTool;
@@ -10,36 +10,26 @@ const tools_teams_1 = require("./tools-teams");
10
10
  const tools_bluebubbles_1 = require("./tools-bluebubbles");
11
11
  const ado_semantic_1 = require("./ado-semantic");
12
12
  const tools_github_1 = require("./tools-github");
13
- const types_1 = require("../mind/friends/types");
14
- const channel_1 = require("../mind/friends/channel");
15
13
  const runtime_1 = require("../nerves/runtime");
14
+ const guardrails_1 = require("./guardrails");
15
+ const identity_1 = require("../heart/identity");
16
+ function safeGetAgentRoot() {
17
+ try {
18
+ return (0, identity_1.getAgentRoot)();
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
16
24
  // Re-export types and constants used by the rest of the codebase
17
25
  var tools_base_2 = require("./tools-base");
18
26
  Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_base_2.tools; } });
19
27
  Object.defineProperty(exports, "finalAnswerTool", { enumerable: true, get: function () { return tools_base_2.finalAnswerTool; } });
20
28
  // All tool definitions in a single registry
21
29
  const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
22
- /** Tool names blocked for untrusted remote contexts. Shared with prompt.ts for restriction messaging. */
23
- exports.REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "edit_file", "glob", "grep"]);
24
- function isTrustedRemoteContext(context) {
25
- if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
26
- return false;
27
- return (0, types_1.isTrustedLevel)(context.friend.trustLevel);
28
- }
29
- function shouldBlockLocalTools(capabilities, context) {
30
- if (!(0, channel_1.isRemoteChannel)(capabilities))
31
- return false;
32
- return !isTrustedRemoteContext(context);
33
- }
34
- function blockedLocalToolMessage() {
35
- return "I can't do that because my trust level with you isn't high enough for local shell/file operations. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
36
- }
37
- function baseToolsForCapabilities(capabilities, context) {
30
+ function baseToolsForCapabilities() {
38
31
  // Use baseToolDefinitions at call time so dynamically-added tools are included
39
- const currentTools = tools_base_1.baseToolDefinitions.map((d) => d.tool);
40
- if (!shouldBlockLocalTools(capabilities, context))
41
- return currentTools;
42
- return currentTools.filter((tool) => !exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
32
+ return tools_base_1.baseToolDefinitions.map((d) => d.tool);
43
33
  }
44
34
  // Apply a single tool preference to a tool schema, returning a new object.
45
35
  function applyPreference(tool, pref) {
@@ -67,8 +57,8 @@ function filterByCapability(toolList, providerCapabilities) {
67
57
  // Teams/integration tools are included only if their integration is in availableIntegrations.
68
58
  // When toolPreferences is provided, matching preferences are appended to tool descriptions.
69
59
  // When providerCapabilities is provided, tools with requiredCapability are filtered.
70
- function getToolsForChannel(capabilities, toolPreferences, context, providerCapabilities) {
71
- const baseTools = baseToolsForCapabilities(capabilities, context);
60
+ function getToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities) {
61
+ const baseTools = baseToolsForCapabilities();
72
62
  const bluebubblesTools = capabilities?.channel === "bluebubbles"
73
63
  ? tools_bluebubbles_1.bluebubblesToolDefinitions.map((d) => d.tool)
74
64
  : [];
@@ -126,16 +116,22 @@ async function execTool(name, args, ctx) {
126
116
  });
127
117
  return `unknown: ${name}`;
128
118
  }
129
- if (shouldBlockLocalTools(ctx?.context?.channel, ctx?.context) && exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
130
- const message = blockedLocalToolMessage();
119
+ // Guardrail check: structural + trust-level
120
+ const guardContext = {
121
+ readPaths: tools_base_1.editFileReadTracker,
122
+ trustLevel: ctx?.context?.friend?.trustLevel,
123
+ agentRoot: safeGetAgentRoot(),
124
+ };
125
+ const guardResult = (0, guardrails_1.guardInvocation)(name, args, guardContext);
126
+ if (!guardResult.allowed) {
131
127
  (0, runtime_1.emitNervesEvent)({
132
128
  level: "warn",
133
- event: "tool.error",
129
+ event: "tool.guardrail_block",
134
130
  component: "tools",
135
- message: "blocked local tool in remote channel",
136
- meta: { name, channel: ctx?.context?.channel?.channel },
131
+ message: "guardrail blocked tool execution",
132
+ meta: { name, reason: guardResult.reason },
137
133
  });
138
- return message;
134
+ return guardResult.reason;
139
135
  }
140
136
  try {
141
137
  const result = await def.handler(args, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.62",
3
+ "version": "0.1.0-alpha.65",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",