@ouro.bot/cli 0.1.0-alpha.71 → 0.1.0-alpha.72

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,14 @@
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.72",
6
+ "changes": [
7
+ "Fix: Compound shell commands (&&, ;, |, ||) are now checked per-subcommand for untrusted users instead of blanket-blocked. Safe compound commands like `ls && pwd` are allowed; only commands containing an untrusted subcommand are denied.",
8
+ "New `ouro config model --agent <name> <model-name>` command: change the active model for any provider. Reads provider from agent.json, updates the model field in secrets.json. Gated at friend trust level.",
9
+ "New `ouro friend update <id> --trust <level>` command: change an existing friend's trust level (stranger, acquaintance, friend, family). Gated at family trust level."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.71",
6
14
  "changes": [
@@ -37,6 +37,7 @@ exports.readAgentConfigForAgent = readAgentConfigForAgent;
37
37
  exports.writeAgentProviderSelection = writeAgentProviderSelection;
38
38
  exports.loadAgentSecrets = loadAgentSecrets;
39
39
  exports.writeProviderCredentials = writeProviderCredentials;
40
+ exports.writeAgentModel = writeAgentModel;
40
41
  exports.collectRuntimeAuthCredentials = collectRuntimeAuthCredentials;
41
42
  exports.resolveHatchCredentials = resolveHatchCredentials;
42
43
  exports.runRuntimeAuthFlow = runRuntimeAuthFlow;
@@ -186,6 +187,26 @@ function writeProviderCredentials(agentName, provider, credentials, deps = {}) {
186
187
  writeSecrets(secretsPath, secrets);
187
188
  return { secretsPath, secrets };
188
189
  }
190
+ const MODEL_FIELD = {
191
+ azure: "modelName",
192
+ minimax: "model",
193
+ anthropic: "model",
194
+ "openai-codex": "model",
195
+ "github-copilot": "model",
196
+ };
197
+ function writeAgentModel(agentName, modelName, deps = {}) {
198
+ const { config } = readAgentConfigForAgent(agentName, deps.bundlesRoot);
199
+ const provider = config.provider;
200
+ const { secretsPath, secrets } = loadAgentSecrets(agentName, deps);
201
+ const providerSecrets = secrets.providers[provider];
202
+ /* v8 ignore next -- fallback: all known providers are in MODEL_FIELD @preserve */
203
+ const fieldName = MODEL_FIELD[provider] ?? "model";
204
+ /* v8 ignore next -- defensive: fieldName always exists in template @preserve */
205
+ const previousModel = providerSecrets[fieldName] ?? "";
206
+ providerSecrets[fieldName] = modelName;
207
+ writeSecrets(secretsPath, secrets);
208
+ return { secretsPath, provider, previousModel };
209
+ }
189
210
  function readCodexAccessToken(homeDir) {
190
211
  const authPath = path.join(homeDir, ".codex", "auth.json");
191
212
  try {
@@ -269,6 +269,7 @@ function usage() {
269
269
  " ouro [up]",
270
270
  " ouro stop|down|status|logs|hatch",
271
271
  " ouro -v|--version",
272
+ " ouro config model --agent <name> <model-name>",
272
273
  " ouro auth --agent <name> [--provider <provider>]",
273
274
  " ouro auth verify --agent <name> [--provider <provider>]",
274
275
  " ouro auth switch --agent <name> --provider <provider>",
@@ -285,6 +286,7 @@ function usage() {
285
286
  " ouro friend list [--agent <name>]",
286
287
  " ouro friend show <id> [--agent <name>]",
287
288
  " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
289
+ " ouro friend update <id> --trust <level> [--agent <name>]",
288
290
  " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
289
291
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
290
292
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
@@ -760,12 +762,51 @@ function parseFriendCommand(args) {
760
762
  ...(agent ? { agent } : {}),
761
763
  };
762
764
  }
765
+ if (sub === "update") {
766
+ const friendId = rest[0];
767
+ if (!friendId)
768
+ throw new Error(`Usage: ouro friend update <id> --trust <level>`);
769
+ let trustLevel;
770
+ /* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
771
+ for (let i = 1; i < rest.length; i++) {
772
+ if (rest[i] === "--trust" && rest[i + 1]) {
773
+ trustLevel = rest[i + 1];
774
+ i += 1;
775
+ }
776
+ }
777
+ /* v8 ignore stop */
778
+ const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
779
+ if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
780
+ throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
781
+ }
782
+ return {
783
+ kind: "friend.update",
784
+ friendId,
785
+ trustLevel: trustLevel,
786
+ ...(agent ? { agent } : {}),
787
+ };
788
+ }
763
789
  if (sub === "link")
764
790
  return parseLinkCommand(rest, "friend.link");
765
791
  if (sub === "unlink")
766
792
  return parseLinkCommand(rest, "friend.unlink");
767
793
  throw new Error(`Usage\n${usage()}`);
768
794
  }
795
+ function parseConfigCommand(args) {
796
+ const { agent, rest: cleaned } = extractAgentFlag(args);
797
+ const [sub, ...rest] = cleaned;
798
+ if (!sub)
799
+ throw new Error(`Usage\n${usage()}`);
800
+ if (sub === "model") {
801
+ if (!agent)
802
+ throw new Error("--agent is required for config model");
803
+ const modelName = rest[0];
804
+ if (!modelName)
805
+ throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
806
+ return { kind: "config.model", agent, modelName };
807
+ }
808
+ throw new Error(`Usage\n${usage()}`);
809
+ }
769
810
  function parseMcpCommand(args) {
770
811
  const [sub, ...rest] = args;
771
812
  if (!sub)
@@ -808,6 +849,8 @@ function parseOuroCommand(args) {
808
849
  return parseReminderCommand(args.slice(1));
809
850
  if (head === "friend")
810
851
  return parseFriendCommand(args.slice(1));
852
+ if (head === "config")
853
+ return parseConfigCommand(args.slice(1));
811
854
  if (head === "mcp")
812
855
  return parseMcpCommand(args.slice(1));
813
856
  if (head === "whoami") {
@@ -1447,6 +1490,19 @@ async function executeFriendCommand(command, store) {
1447
1490
  });
1448
1491
  return `created: ${id} (${command.name}, ${trustLevel})`;
1449
1492
  }
1493
+ if (command.kind === "friend.update") {
1494
+ const current = await store.get(command.friendId);
1495
+ if (!current)
1496
+ return `friend not found: ${command.friendId}`;
1497
+ const now = new Date().toISOString();
1498
+ await store.put(command.friendId, {
1499
+ ...current,
1500
+ trustLevel: command.trustLevel,
1501
+ role: command.trustLevel,
1502
+ updatedAt: now,
1503
+ });
1504
+ return `updated: ${command.friendId} → trust=${command.trustLevel}`;
1505
+ }
1450
1506
  if (command.kind === "friend.link") {
1451
1507
  const current = await store.get(command.friendId);
1452
1508
  if (!current)
@@ -1673,7 +1729,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1673
1729
  }
1674
1730
  // ── friend subcommands (local, no daemon socket needed) ──
1675
1731
  if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
1676
- command.kind === "friend.link" || command.kind === "friend.unlink") {
1732
+ command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
1677
1733
  /* v8 ignore start -- production default: requires full identity setup @preserve */
1678
1734
  let store = deps.friendStore;
1679
1735
  if (!store) {
@@ -1740,6 +1796,17 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1740
1796
  return message;
1741
1797
  }
1742
1798
  /* v8 ignore stop */
1799
+ // ── config model (local, no daemon socket needed) ──
1800
+ /* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
1801
+ if (command.kind === "config.model") {
1802
+ const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, command.modelName);
1803
+ const message = previousModel
1804
+ ? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
1805
+ : `set ${command.agent} model on ${provider}: ${command.modelName}`;
1806
+ deps.writeStdout(message);
1807
+ return message;
1808
+ }
1809
+ /* v8 ignore stop */
1743
1810
  // ── whoami (local, no daemon socket needed) ──
1744
1811
  if (command.kind === "whoami") {
1745
1812
  if (command.agent) {
@@ -178,8 +178,10 @@ my bones give me the \`ouro\` cli. always pass \`--agent ${agentName}\`:
178
178
  ouro task update --agent ${agentName} <id> <status>
179
179
  ouro friend list --agent ${agentName}
180
180
  ouro friend show --agent ${agentName} <id>
181
+ ouro friend update --agent ${agentName} <id> --trust <level>
181
182
  ouro session list --agent ${agentName}
182
183
  ouro reminder create --agent ${agentName} <title> --body <body>
184
+ ouro config model --agent ${agentName} <model-name>
183
185
  ouro auth --agent ${agentName} --provider <provider>
184
186
  ouro auth verify --agent ${agentName} [--provider <provider>]
185
187
  ouro auth switch --agent ${agentName} --provider <provider>
@@ -48,7 +48,6 @@ const REASONS = {
48
48
  readBeforeOverwrite: "i need to read that file first before i can overwrite it.",
49
49
  protectedPath: "that path is protected — i can read it but not modify it.",
50
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
51
  // Trust reasons (vary by relationship)
53
52
  needsTrust: "i'd need a closer friend to vouch for you before i can do that.",
54
53
  needsTrustForWrite: "i'd need a closer friend to vouch for you before i can write files outside my home.",
@@ -90,9 +89,6 @@ function splitShellCommands(command) {
90
89
  return [command];
91
90
  return command.split(COMPOUND_SEPARATORS).filter(Boolean);
92
91
  }
93
- function isCompoundCommand(command) {
94
- return SUBSHELL_PATTERN.test(command) || splitShellCommands(command).length > 1;
95
- }
96
92
  // --- shell commands that write to protected paths ---
97
93
  function shellWritesToProtectedPath(command) {
98
94
  const redirectMatch = command.match(/>\s*(\S+)/);
@@ -169,7 +165,9 @@ exports.OURO_CLI_TRUST_MANIFEST = {
169
165
  "friend list": "friend",
170
166
  "friend show": "friend",
171
167
  "friend create": "friend",
168
+ "friend update": "family",
172
169
  "reminder create": "friend",
170
+ "config model": "friend",
173
171
  "mcp list": "acquaintance",
174
172
  "mcp call": "friend",
175
173
  auth: "family",
@@ -234,11 +232,21 @@ function checkSingleShellCommandTrust(command, trustLevel) {
234
232
  return deny(REASONS.needsTrust);
235
233
  }
236
234
  function checkShellTrustGuardrails(command, trustLevel) {
237
- // Compound commands: for untrusted users, reject entirely.
238
- // This prevents "ouro whoami && rm -rf /" from smuggling dangerous commands.
239
- if (isCompoundCommand(command))
240
- return deny(REASONS.compoundCommand);
241
- return checkSingleShellCommandTrust(command, trustLevel);
235
+ // Subshell patterns ($(), backticks) can't be reliably split — check as single command
236
+ /* v8 ignore next -- subshell branch: tested via guardrails.test.ts @preserve */
237
+ if (SUBSHELL_PATTERN.test(command)) {
238
+ return checkSingleShellCommandTrust(command, trustLevel);
239
+ }
240
+ // Compound commands: check each subcommand individually
241
+ const subcommands = splitShellCommands(command);
242
+ if (subcommands.length === 0)
243
+ return checkSingleShellCommandTrust(command, trustLevel);
244
+ for (const sub of subcommands) {
245
+ const result = checkSingleShellCommandTrust(sub, trustLevel);
246
+ if (!result.allowed)
247
+ return result;
248
+ }
249
+ return allow;
242
250
  }
243
251
  function checkWriteTrustGuardrails(toolName, args, context) {
244
252
  if (toolName !== "write_file" && toolName !== "edit_file")
@@ -103,7 +103,7 @@ async function execTool(name, args, ctx) {
103
103
  event: "tool.start",
104
104
  component: "tools",
105
105
  message: "tool execution started",
106
- meta: { name },
106
+ meta: { name, ...(name === "shell" && args.command ? { command: args.command } : {}) },
107
107
  });
108
108
  // Look up from combined registry
109
109
  const def = allDefinitions.find((d) => d.tool.function.name === name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.71",
3
+ "version": "0.1.0-alpha.72",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",