@ouro.bot/cli 0.1.0-alpha.894 → 0.1.0-alpha.897

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.
Files changed (35) hide show
  1. package/changelog.json +18 -0
  2. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  3. package/deploy/unraid/sanctuary.xml +1 -1
  4. package/dist/heart/daemon/runtime-logging.js +1 -1
  5. package/dist/heart/daemon/sense-manager.js +20 -20
  6. package/dist/heart/identity.js +4 -4
  7. package/dist/heart/private-runtime/policy.js +4 -4
  8. package/dist/heart/sense-truth.js +2 -2
  9. package/dist/heart/{cmux-config.js → shepherd-config.js} +24 -24
  10. package/dist/heart/steward-policy.js +0 -26
  11. package/dist/heart/turn-context.js +5 -5
  12. package/dist/mind/prompt.js +5 -5
  13. package/dist/repertoire/bitwarden-store.js +153 -7
  14. package/dist/repertoire/credential-access.js +27 -23
  15. package/dist/repertoire/guardrails.js +4 -4
  16. package/dist/repertoire/tools-shepherd.js +181 -0
  17. package/dist/repertoire/tools.js +4 -4
  18. package/dist/repertoire/vault-unlock.js +59 -1
  19. package/dist/senses/{cmux → shepherd}/client.js +3 -3
  20. package/dist/senses/shepherd/cmux.js +332 -0
  21. package/dist/senses/shepherd/host.js +2 -0
  22. package/dist/senses/shepherd/judge.js +98 -0
  23. package/dist/senses/{cmux → shepherd}/redact.js +3 -3
  24. package/dist/senses/shepherd/returns.js +79 -0
  25. package/dist/senses/shepherd/sense.js +288 -0
  26. package/dist/senses/{cmux-entry.js → shepherd-entry.js} +10 -9
  27. package/npm-shrinkwrap.json +2 -2
  28. package/package.json +1 -1
  29. package/dist/repertoire/tools-cmux.js +0 -396
  30. package/dist/senses/cmux/answer.js +0 -191
  31. package/dist/senses/cmux/attention.js +0 -265
  32. package/dist/senses/cmux/casebook.js +0 -416
  33. package/dist/senses/cmux/floor.js +0 -407
  34. package/dist/senses/cmux/principles.js +0 -72
  35. package/dist/senses/cmux/sense.js +0 -287
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
44
  exports.BitwardenCredentialStore = void 0;
45
45
  exports.sanitizeCredentialErrorDetail = sanitizeCredentialErrorDetail;
46
+ exports.readBwCliVersion = readBwCliVersion;
46
47
  const node_child_process_1 = require("node:child_process");
47
48
  const fs = __importStar(require("node:fs"));
48
49
  const path = __importStar(require("node:path"));
@@ -169,6 +170,7 @@ const BW_LOCK_STALE_MS = BW_LOCK_TIMEOUT_MS * 2;
169
170
  const BW_DATA_FILENAME = "data.json";
170
171
  const BW_SYNC_MARKER_FILENAME = ".ouro-last-sync";
171
172
  const BW_SYNC_FRESH_MS = 60_000;
173
+ const BW_TERMINAL_LOGIN_ERROR_RETRY_MS = 15 * 60_000;
172
174
  /** In-process async mutex keyed by appDataDir. */
173
175
  const inProcessLocks = new Map();
174
176
  function isPidAlive(pid) {
@@ -365,6 +367,58 @@ function isBwNotInstalled(err) {
365
367
  const code = err.code;
366
368
  return code === "ENOENT" || /\bspawn\b.*\benoent\b/.test(msg) || msg.includes("command not found");
367
369
  }
370
+ /**
371
+ * Read the installed @bitwarden/cli version from the package that owns the
372
+ * resolved `bw` binary. Returns null when the binary is not an npm install.
373
+ */
374
+ function readBwCliVersion(bwBinaryPath) {
375
+ let dir;
376
+ try {
377
+ dir = path.dirname(fs.realpathSync(bwBinaryPath));
378
+ }
379
+ catch {
380
+ return null;
381
+ }
382
+ for (let depth = 0; depth < 4; depth++) {
383
+ try {
384
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
385
+ if (pkg.name === "@bitwarden/cli" && typeof pkg.version === "string")
386
+ return pkg.version;
387
+ }
388
+ catch {
389
+ // Keep walking up toward the package root.
390
+ }
391
+ dir = path.dirname(dir);
392
+ }
393
+ return null;
394
+ }
395
+ /** Another process holds the profile; the profile itself is not at fault. */
396
+ function isBwLockTimeout(err) {
397
+ return err.message.includes("bw CLI lock timeout");
398
+ }
399
+ /**
400
+ * A local bw step (unlock of an already-authenticated profile) failed for a
401
+ * reason that says nothing about the vault server: the profile on disk is
402
+ * unusable. Network, lock and missing-binary failures are not this, and
403
+ * neither is a rejected unlock secret.
404
+ */
405
+ class LocalProfileUnusableError extends Error {
406
+ localError;
407
+ constructor(localError) {
408
+ super(localError.message);
409
+ this.localError = localError;
410
+ }
411
+ }
412
+ function asLocalProfileFailure(err) {
413
+ if (isTransientError(err) || isBwNotInstalled(err) || isBwLockTimeout(err) || isBwInvalidUnlockSecretMessage(err.message)) {
414
+ return err;
415
+ }
416
+ return new LocalProfileUnusableError(err);
417
+ }
418
+ /** A bw version as a safe directory name component. */
419
+ function bwProfileDirName(version) {
420
+ return `bw-${version.replace(/[^A-Za-z0-9._-]/g, "_")}`;
421
+ }
368
422
  /** Check if the error is transient (network/timeout) and worth retrying. */
369
423
  function isTransientError(err) {
370
424
  const msg = err.message.toLowerCase();
@@ -372,7 +426,18 @@ function isTransientError(err) {
372
426
  msg.includes("etimedout") ||
373
427
  msg.includes("enotfound") ||
374
428
  msg.includes("socket hang up") ||
429
+ msg.includes("econnreset") ||
430
+ msg.includes("eai_again") ||
431
+ msg.includes("enetunreach") ||
432
+ msg.includes("ehostunreach") ||
433
+ msg.includes("fetch failed") ||
434
+ msg.includes("429") ||
435
+ msg.includes("too many requests") ||
436
+ msg.includes("502") ||
375
437
  msg.includes("503") ||
438
+ msg.includes("504") ||
439
+ msg.includes("bad gateway") ||
440
+ msg.includes("gateway timeout") ||
376
441
  msg.includes("server unavailable") ||
377
442
  msg.includes("timed out"));
378
443
  }
@@ -491,17 +556,22 @@ class BitwardenCredentialStore {
491
556
  serverUrl;
492
557
  email;
493
558
  masterPassword;
559
+ /** The agent's bw folder; each bw version keeps its own profile under it. */
560
+ agentDataDir;
561
+ /** The profile directory for the running bw version (BITWARDENCLI_APPDATA_DIR). */
494
562
  appDataDir;
495
563
  onInvalidUnlockSecret;
496
564
  onLoginSuccess;
497
565
  sessionToken = null;
498
566
  terminalLoginError = null;
567
+ terminalLoginErrorAt = 0;
499
568
  bwBinaryPath = "bw";
500
569
  structuredItemCache = null;
501
570
  constructor(serverUrl, email, masterPassword, options = {}) {
502
571
  this.serverUrl = serverUrl;
503
572
  this.email = email;
504
573
  this.masterPassword = masterPassword;
574
+ this.agentDataDir = options.appDataDir;
505
575
  this.appDataDir = options.appDataDir;
506
576
  this.onInvalidUnlockSecret = options.onInvalidUnlockSecret;
507
577
  this.onLoginSuccess = options.onLoginSuccess;
@@ -543,11 +613,21 @@ class BitwardenCredentialStore {
543
613
  */
544
614
  async login() {
545
615
  if (this.terminalLoginError) {
546
- throw this.terminalLoginError;
616
+ // A long-lived process retries a terminal failure after a while instead
617
+ // of holding it until restart; rejected secrets are paused separately.
618
+ if (Date.now() - this.terminalLoginErrorAt < BW_TERMINAL_LOGIN_ERROR_RETRY_MS) {
619
+ throw this.terminalLoginError;
620
+ }
621
+ this.terminalLoginError = null;
547
622
  }
548
623
  // Ensure bw CLI is installed before any bw commands
549
624
  this.bwBinaryPath = await (0, bw_installer_1.ensureBwCli)();
550
- if (this.appDataDir) {
625
+ if (this.agentDataDir) {
626
+ // Each bw version gets its own profile directory, so machines with
627
+ // several bw installs (one per Node version) never migrate or fight
628
+ // over one another's state.
629
+ const bwVersion = readBwCliVersion(this.bwBinaryPath);
630
+ this.appDataDir = bwVersion ? path.join(this.agentDataDir, bwProfileDirName(bwVersion)) : this.agentDataDir;
551
631
  fs.mkdirSync(this.appDataDir, { recursive: true, mode: 0o700 });
552
632
  }
553
633
  let lastError;
@@ -562,7 +642,7 @@ class BitwardenCredentialStore {
562
642
  lastError = err instanceof Error ? err : new Error(String(err));
563
643
  // Don't retry non-transient errors (auth failures, bw not installed)
564
644
  if (!isTransientError(lastError)) {
565
- this.terminalLoginError = lastError;
645
+ this.setTerminalLoginError(lastError);
566
646
  if (isBwInvalidUnlockSecretMessage(lastError.message)) {
567
647
  await this.notifyInvalidUnlockSecret(lastError);
568
648
  }
@@ -581,15 +661,76 @@ class BitwardenCredentialStore {
581
661
  await delay(backoffMs);
582
662
  }
583
663
  }
584
- this.terminalLoginError = lastError;
664
+ this.setTerminalLoginError(lastError);
585
665
  /* v8 ignore next -- invalid unlock errors are non-transient and exit through the non-retry path above @preserve */
586
666
  if (isBwInvalidUnlockSecretMessage(lastError.message)) {
587
667
  await this.notifyInvalidUnlockSecret(lastError);
588
668
  }
589
669
  throw lastError;
590
670
  }
591
- /** Single login attempt — called by login() retry loop. */
671
+ setTerminalLoginError(error) {
672
+ this.terminalLoginError = error;
673
+ this.terminalLoginErrorAt = Date.now();
674
+ }
675
+ /**
676
+ * Single login attempt — called by login() retry loop.
677
+ *
678
+ * The bw profile directory is a disposable cache; the durable secret is the
679
+ * machine's vault unlock material. When a local step fails on an
680
+ * authenticated profile (a corrupt or half-migrated profile, a missing
681
+ * session), the profile is moved aside and rebuilt with one fresh login.
682
+ * Server steps (login, sync), network failures, lock contention and
683
+ * rejected secrets never discard the profile, so a bad secret or a flaky
684
+ * server costs at most one server login per attempt.
685
+ */
592
686
  async loginAttempt() {
687
+ try {
688
+ await this.loginWithLocalProfile();
689
+ }
690
+ catch (error) {
691
+ if (!(error instanceof LocalProfileUnusableError))
692
+ throw error;
693
+ await this.discardLocalProfile(error.localError);
694
+ try {
695
+ await this.loginWithLocalProfile();
696
+ }
697
+ catch (retryError) {
698
+ throw retryError instanceof LocalProfileUnusableError ? retryError.localError : retryError;
699
+ }
700
+ }
701
+ }
702
+ /**
703
+ * Move the local bw profile aside so the next bw command starts from an
704
+ * empty profile. The previous data file is kept as one `.discarded` copy
705
+ * for diagnosis and is overwritten by the next discard.
706
+ */
707
+ async discardLocalProfile(cause) {
708
+ const appDataDir = this.appDataDir;
709
+ this.sessionToken = null;
710
+ this.structuredItemCache = null;
711
+ await withBwLock(appDataDir, async () => {
712
+ const dataPath = path.join(appDataDir, BW_DATA_FILENAME);
713
+ try {
714
+ fs.renameSync(dataPath, `${dataPath}.discarded`);
715
+ }
716
+ catch {
717
+ // Already gone: another process discarded it first, which is the same outcome.
718
+ }
719
+ fs.rmSync(path.join(appDataDir, BW_SYNC_MARKER_FILENAME), { force: true });
720
+ });
721
+ (0, runtime_1.emitNervesEvent)({
722
+ level: "warn",
723
+ event: "repertoire.bw_local_profile_discarded",
724
+ component: "repertoire",
725
+ message: "discarded unusable local bw profile; logging in fresh with the saved unlock secret",
726
+ meta: {
727
+ email: this.email,
728
+ serverUrl: this.serverUrl,
729
+ error: sanitizeCredentialErrorDetail(cause.message),
730
+ },
731
+ });
732
+ }
733
+ async loginWithLocalProfile() {
593
734
  let status = await this.readStatus();
594
735
  if (this.shouldRebuildLocalProfile(status)) {
595
736
  await this.rebuildLocalProfile("local bw profile did not match requested vault", status);
@@ -692,7 +833,12 @@ class BitwardenCredentialStore {
692
833
  const err = error;
693
834
  if (!isBwAlreadyLoggedInError(err))
694
835
  throw err;
695
- return this.execBwWithPasswordEnv(["unlock", "--raw"]);
836
+ }
837
+ try {
838
+ return await this.execBwWithPasswordEnv(["unlock", "--raw"]);
839
+ }
840
+ catch (error) {
841
+ throw asLocalProfileFailure(error);
696
842
  }
697
843
  }
698
844
  forgetLocalSyncMarker() {
@@ -754,7 +900,7 @@ class BitwardenCredentialStore {
754
900
  catch (error) {
755
901
  const err = error;
756
902
  if (!isBwInvalidUnlockSecretMessage(err.message))
757
- throw err;
903
+ throw asLocalProfileFailure(err);
758
904
  await this.rebuildLocalProfile(reason, status, err);
759
905
  /* v8 ignore next -- defensive: rebuildLocalProfile always stores a string session token on success @preserve */
760
906
  return this.sessionToken ?? "";
@@ -46,15 +46,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.getCredentialStore = getCredentialStore;
47
47
  exports.probeCredentialVaultAccess = probeCredentialVaultAccess;
48
48
  exports.resetCredentialStore = resetCredentialStore;
49
- const crypto = __importStar(require("node:crypto"));
50
49
  const fs = __importStar(require("node:fs"));
51
- const os = __importStar(require("node:os"));
52
50
  const path = __importStar(require("node:path"));
53
51
  const runtime_1 = require("../nerves/runtime");
54
52
  const identity = __importStar(require("../heart/identity"));
55
53
  const bitwarden_store_1 = require("./bitwarden-store");
56
54
  const vault_unlock_1 = require("./vault-unlock");
57
55
  let stores = new Map();
56
+ function rejectedUnlockMessage(agentName, reason) {
57
+ return [
58
+ `The vault rejected the unlock secret saved on this machine for ${agentName} (${reason}).`,
59
+ "The saved secret was kept. If the vault's unlock secret changed, run",
60
+ `\`ouro vault unlock --agent ${agentName}\` once with the current secret.`,
61
+ ].join(" ");
62
+ }
58
63
  function loadVaultSectionForAgent(agentName) {
59
64
  const configPath = path.join(identity.getAgentRoot(agentName), "agent.json");
60
65
  try {
@@ -65,14 +70,6 @@ function loadVaultSectionForAgent(agentName) {
65
70
  return { configPath };
66
71
  }
67
72
  }
68
- function bitwardenAppDataDir(agentName, vaultConfig, homeDir = os.homedir()) {
69
- const digest = crypto
70
- .createHash("sha256")
71
- .update(`${agentName}:${vaultConfig.serverUrl}:${vaultConfig.email}`)
72
- .digest("hex")
73
- .slice(0, 24);
74
- return path.join(homeDir, ".ouro-cli", "bitwarden", digest);
75
- }
76
73
  function getCredentialStore(agentNameInput) {
77
74
  const agentName = agentNameInput ?? identity.getAgentName();
78
75
  if (agentName === "SerpentGuide") {
@@ -87,6 +84,11 @@ function getCredentialStore(agentNameInput) {
87
84
  const cached = stores.get(cacheKey);
88
85
  if (cached)
89
86
  return cached;
87
+ const agentDataDir = (0, vault_unlock_1.bitwardenAppDataDir)(agentName, vaultConfig);
88
+ const rejection = (0, vault_unlock_1.readVaultUnlockRejection)(agentDataDir);
89
+ if (rejection) {
90
+ throw new Error(rejectedUnlockMessage(agentName, rejection.reason));
91
+ }
90
92
  const unlock = (0, vault_unlock_1.readVaultUnlockSecret)({
91
93
  agentName,
92
94
  email: vaultConfig.email,
@@ -94,25 +96,22 @@ function getCredentialStore(agentNameInput) {
94
96
  });
95
97
  const unlockConfig = { agentName, email: vaultConfig.email, serverUrl: vaultConfig.serverUrl };
96
98
  const unlockSource = unlock.source ?? unlockConfig;
97
- let invalidUnlockCleared = false;
98
99
  let canonicalUnlockStored = false;
99
100
  const store = new bitwarden_store_1.BitwardenCredentialStore(vaultConfig.serverUrl, vaultConfig.email, unlock.secret, {
100
- appDataDir: bitwardenAppDataDir(agentName, vaultConfig),
101
+ appDataDir: agentDataDir,
101
102
  onInvalidUnlockSecret: (error) => {
102
- if (invalidUnlockCleared)
103
- return;
104
- invalidUnlockCleared = true;
105
- (0, vault_unlock_1.clearVaultUnlockSecret)({
106
- agentName,
107
- email: unlockSource.email,
108
- serverUrl: unlockSource.serverUrl,
109
- });
103
+ // The vault rejected the saved secret. Keep the saved secret: deleting
104
+ // the only durable copy turns any misclassified bw or server error
105
+ // into a human prompt. Record the rejection on disk so no process
106
+ // retries it against the server; `ouro vault unlock` or any
107
+ // successful login clears the record.
110
108
  stores.delete(cacheKey);
109
+ (0, vault_unlock_1.recordVaultUnlockRejection)(agentDataDir, error.message);
111
110
  (0, runtime_1.emitNervesEvent)({
112
111
  level: "warn",
113
- event: "repertoire.credential_store_invalid_unlock_cleared",
112
+ event: "repertoire.credential_store_unlock_rejected",
114
113
  component: "repertoire",
115
- message: "cleared rejected local vault unlock material",
114
+ message: "vault rejected the saved local unlock secret; kept it and paused retries until a new secret is saved",
116
115
  meta: {
117
116
  agentName,
118
117
  serverUrl: vaultConfig.serverUrl,
@@ -123,6 +122,7 @@ function getCredentialStore(agentNameInput) {
123
122
  });
124
123
  },
125
124
  onLoginSuccess: () => {
125
+ (0, vault_unlock_1.clearVaultUnlockRejection)(agentDataDir);
126
126
  if (canonicalUnlockStored)
127
127
  return;
128
128
  if (unlockSource.serverUrl === vaultConfig.serverUrl && unlockSource.email === vaultConfig.email)
@@ -170,7 +170,11 @@ async function probeCredentialVaultAccess(agentNameInput, unlockSecret, options
170
170
  throw new Error((0, vault_unlock_1.credentialVaultNotConfiguredError)(agentName, configPath));
171
171
  }
172
172
  const vaultConfig = identity.resolveVaultConfig(agentName, vault);
173
- const store = new bitwarden_store_1.BitwardenCredentialStore(vaultConfig.serverUrl, vaultConfig.email, unlockSecret, { appDataDir: bitwardenAppDataDir(agentName, vaultConfig, options.homeDir) });
173
+ const agentDataDir = (0, vault_unlock_1.bitwardenAppDataDir)(agentName, vaultConfig, options.homeDir);
174
+ const store = new bitwarden_store_1.BitwardenCredentialStore(vaultConfig.serverUrl, vaultConfig.email, unlockSecret, {
175
+ appDataDir: agentDataDir,
176
+ onLoginSuccess: () => (0, vault_unlock_1.clearVaultUnlockRejection)(agentDataDir),
177
+ });
174
178
  await store.get("__ouro_vault_probe__");
175
179
  }
176
180
  function resetCredentialStore() {
@@ -441,9 +441,9 @@ const TRAVEL_TRUSTED_TOOLS = new Set(["weather_lookup", "travel_advisory", "geoc
441
441
  const A2A_TRUSTED_TOOLS = new Set(["a2a_list_peers", "a2a_send_message", "a2a_get_task"]);
442
442
  const COMMERCE_FAMILY_TOOLS = new Set(["commerce_checkout_preview", "commerce_checkout_commit", "commerce_receipt_get", "commerce_access_log"]);
443
443
  const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
444
- // The cmux tools read the owner's terminals and speak to them through their desktop, so only family may use them.
444
+ // The Shepherd tools read the owner's terminals and speak to them through their desktop, so only family may use them.
445
445
  // A call with no known trust level fails closed.
446
- const CMUX_FAMILY_TOOLS = new Set(["cmux_overview", "cmux_read", "cmux_signal", "cmux_reply_once", "cmux_correct"]);
446
+ const SHEPHERD_FAMILY_TOOLS = new Set(["shepherd_overview", "shepherd_read", "shepherd_signal"]);
447
447
  const MAIL_FAMILY_TOOLS = new Set(["mail_screener", "mail_decide", "mail_access_log", "mail_send", "mail_index_refresh"]);
448
448
  const MAIL_DELEGATED_READ_TOOLS = new Set(["mail_recent", "mail_search"]);
449
449
  function opaqueAnsiShellAtoms(command) {
@@ -1184,8 +1184,8 @@ function checkTrustLevelGuardrails(toolName, args, context) {
1184
1184
  const mailResult = mailTrustGuardrail(toolName, args, context);
1185
1185
  if (!mailResult.allowed)
1186
1186
  return mailResult;
1187
- if (CMUX_FAMILY_TOOLS.has(toolName) && context.trustLevel !== "family") {
1188
- return deny("the owner's cmux terminals require family trust.");
1187
+ if (SHEPHERD_FAMILY_TOOLS.has(toolName) && context.trustLevel !== "family") {
1188
+ return deny("the owner's terminals require family trust.");
1189
1189
  }
1190
1190
  // Credential tools have their own trust rules that apply at all levels
1191
1191
  const credentialResult = checkCredentialTrustGuardrails(toolName, context);
@@ -0,0 +1,181 @@
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.shepherdToolDefinitions = exports.shepherdSignalToolDefinition = exports.shepherdReadToolDefinition = exports.shepherdOverviewToolDefinition = void 0;
37
+ exports.shepherdToolsEnabled = shepherdToolsEnabled;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const runtime_credentials_1 = require("../heart/runtime-credentials");
41
+ const shepherd_config_1 = require("../heart/shepherd-config");
42
+ const runtime_1 = require("../nerves/runtime");
43
+ const client_1 = require("../senses/shepherd/client");
44
+ const cmux_1 = require("../senses/shepherd/cmux");
45
+ const redact_1 = require("../senses/shepherd/redact");
46
+ const returns_1 = require("../senses/shepherd/returns");
47
+ /**
48
+ * Shepherd's tools: every coding-agent session on this machine's terminal host with what Shepherd
49
+ * last decided about it (`shepherd_overview`), what a session shows (`shepherd_read`), and a status
50
+ * line or notification for the human (`shepherd_signal`).
51
+ */
52
+ const STATUS_MAX_CHARS = 120;
53
+ const READ_MAX_CHARS = 16_000;
54
+ function text(value) {
55
+ return typeof value === "string" ? value : "";
56
+ }
57
+ function shepherdToolsEnabled(agentRoot) {
58
+ if (!agentRoot)
59
+ return false;
60
+ try {
61
+ const config = JSON.parse(fs.readFileSync(path.join(agentRoot, "agent.json"), "utf-8"));
62
+ return config.senses?.shepherd?.enabled === true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ function hostFor(agentName) {
69
+ const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agentName);
70
+ const resolved = (0, shepherd_config_1.resolveShepherdConnection)(agentName, machine.ok ? machine.config : {});
71
+ return resolved.ok ? (0, cmux_1.createCmuxHost)((0, client_1.createCmuxClient)(resolved.connection, { agentName })) : resolved.error;
72
+ }
73
+ async function withHost(tool, ctx, run) {
74
+ if (!ctx?.agentName || !ctx.agentRoot)
75
+ return JSON.stringify({ error: "the Shepherd tools need an agent runtime" });
76
+ const host = hostFor(ctx.agentName);
77
+ if (typeof host === "string")
78
+ return JSON.stringify({ error: host });
79
+ try {
80
+ const result = await run(host);
81
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.shepherd_tool", message: "ran a Shepherd tool", meta: { tool } });
82
+ return JSON.stringify(result);
83
+ }
84
+ catch (error) {
85
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "repertoire", event: "repertoire.shepherd_tool_error", message: "Shepherd tool call failed", meta: { tool, error: error.message } });
86
+ return JSON.stringify({ error: error.message });
87
+ }
88
+ }
89
+ exports.shepherdOverviewToolDefinition = {
90
+ tool: {
91
+ type: "function",
92
+ function: {
93
+ name: "shepherd_overview",
94
+ description: "Every terminal session on this machine's terminal host (cmux), with the coding agent there when known, and the last time Shepherd judged a return of control in it: when, what kind (premature, gate, done, unclear, loop_guard, error), whether it answered, and why. Read-only.",
95
+ parameters: { type: "object", properties: {}, additionalProperties: false },
96
+ },
97
+ },
98
+ handler: (_args, ctx) => withHost("shepherd_overview", ctx, async (host) => {
99
+ const last = new Map();
100
+ for (const record of (0, returns_1.readReturns)(ctx.agentRoot))
101
+ last.set(record.session, record);
102
+ return {
103
+ host: host.name,
104
+ sessions: (await host.list()).map((session) => {
105
+ const record = last.get(session.id);
106
+ return { ...session, ...(record ? { lastReturn: { at: record.at, kind: record.kind, action: record.action, reason: record.reason, reply: record.reply, task: record.task } } : {}) };
107
+ }),
108
+ };
109
+ }),
110
+ summaryKeys: [],
111
+ riskProfile: { mutates: "none", risk: "low" },
112
+ };
113
+ function boundedLines(value) {
114
+ const parsed = typeof value === "number" ? value : Number.parseInt(text(value), 10);
115
+ return Number.isFinite(parsed) ? Math.min(400, Math.max(1, Math.trunc(parsed))) : 60;
116
+ }
117
+ exports.shepherdReadToolDefinition = {
118
+ tool: {
119
+ type: "function",
120
+ function: {
121
+ name: "shepherd_read",
122
+ description: "Read the text of one terminal session (what a coding agent shows now and what it did recently). Secret-shaped strings are redacted and the text is bounded. Read-only. Take the session id or ref from shepherd_overview.",
123
+ parameters: {
124
+ type: "object",
125
+ properties: {
126
+ session: { type: "string", description: "Session id or ref (surface:12) from shepherd_overview." },
127
+ lines: { type: "number", description: "How many lines, 1-400. Default 60." },
128
+ },
129
+ required: ["session"],
130
+ additionalProperties: false,
131
+ },
132
+ },
133
+ },
134
+ handler: (args, ctx) => {
135
+ const session = text(args.session).trim();
136
+ if (!session)
137
+ return Promise.resolve(JSON.stringify({ error: "name a session id or ref from shepherd_overview" }));
138
+ return withHost("shepherd_read", ctx, async (host) => {
139
+ const screen = (0, redact_1.redactSecrets)(await host.read(session, boundedLines(args.lines)));
140
+ return { session, text: screen.slice(-READ_MAX_CHARS), truncated: screen.length > READ_MAX_CHARS };
141
+ });
142
+ },
143
+ summaryKeys: ["session"],
144
+ riskProfile: { mutates: "none", risk: "low" },
145
+ };
146
+ exports.shepherdSignalToolDefinition = {
147
+ tool: {
148
+ type: "function",
149
+ function: {
150
+ name: "shepherd_signal",
151
+ description: `Tell the human something through the terminal host. status sets Shepherd's one-line status on that session's workspace (at most ${STATUS_MAX_CHARS} characters; an empty string clears it). notify_title with notify_body also posts a desktop notification there. It never types into a terminal.`,
152
+ parameters: {
153
+ type: "object",
154
+ properties: {
155
+ session: { type: "string", description: "Session id or ref from shepherd_overview." },
156
+ status: { type: "string", description: "One line for the status, or an empty string to clear it." },
157
+ notify_title: { type: "string", description: "Desktop notification title." },
158
+ notify_body: { type: "string", description: "Desktop notification body." },
159
+ },
160
+ required: ["session", "status"],
161
+ additionalProperties: false,
162
+ },
163
+ },
164
+ },
165
+ handler: (args, ctx) => {
166
+ const session = text(args.session).trim();
167
+ const status = text(args.status).trim();
168
+ const title = text(args.notify_title).trim();
169
+ if (!session)
170
+ return Promise.resolve(JSON.stringify({ error: "name a session id or ref from shepherd_overview" }));
171
+ if (status.length > STATUS_MAX_CHARS || /[\r\n]/.test(status))
172
+ return Promise.resolve(JSON.stringify({ error: `status must be one line of at most ${STATUS_MAX_CHARS} characters` }));
173
+ return withHost("shepherd_signal", ctx, async (host) => {
174
+ await host.signal(session, status || null, title ? { title, body: text(args.notify_body) } : undefined);
175
+ return { session, status: status ? "set" : "cleared", notified: !!title };
176
+ });
177
+ },
178
+ summaryKeys: ["session", "status"],
179
+ riskProfile: { mutates: "external_side_effect", risk: "high", reason: "changes what the terminal host shows the human (a status line or a desktop notification)" },
180
+ };
181
+ exports.shepherdToolDefinitions = [exports.shepherdOverviewToolDefinition, exports.shepherdReadToolDefinition, exports.shepherdSignalToolDefinition];
@@ -37,7 +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 tools_house_care_1 = require("./tools-house-care");
40
- const tools_cmux_1 = require("./tools-cmux");
40
+ const tools_shepherd_1 = require("./tools-shepherd");
41
41
  const steward_policy_1 = require("../heart/steward-policy");
42
42
  const relationship_authorization_1 = require("./relationship-authorization");
43
43
  const tool_approval_1 = require("../heart/tool-approval");
@@ -98,7 +98,7 @@ Object.defineProperty(exports, "speakTool", { enumerable: true, get: function ()
98
98
  var tools_surface_2 = require("./tools-surface");
99
99
  Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: function () { return tools_surface_2.surfaceToolDef; } });
100
100
  // All tool definitions in a single registry
101
- const additionalDefinitions = [...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...tools_house_care_1.houseCareToolDefinitions, tools_surface_1.surfaceToolDefinition, ...tools_cmux_1.cmuxToolDefinitions];
101
+ const additionalDefinitions = [...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...tools_house_care_1.houseCareToolDefinitions, tools_surface_1.surfaceToolDefinition, ...tools_shepherd_1.shepherdToolDefinitions];
102
102
  const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
103
103
  class ToolSelectionError extends Error {
104
104
  }
@@ -172,8 +172,8 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
172
172
  });
173
173
  ordinary = [
174
174
  ...tools_base_1.baseToolDefinitions,
175
- // The cmux sense's tools exist only for an agent that turned the sense on in agent.json.
176
- ...((0, tools_cmux_1.cmuxToolsEnabled)(context?.agentRoot ?? safeGetAgentRoot()) ? tools_cmux_1.cmuxToolDefinitions : []),
175
+ // The Shepherd tools exist only for an agent that turned the sense on in agent.json.
176
+ ...((0, tools_shepherd_1.shepherdToolsEnabled)(context?.agentRoot ?? safeGetAgentRoot()) ? tools_shepherd_1.shepherdToolDefinitions : []),
177
177
  ...(capabilities?.channel === "bluebubbles" ? tools_bluebubbles_1.bluebubblesToolDefinitions : []),
178
178
  ...(capabilities?.channel === "voice" ? tools_voice_1.voiceToolDefinitions : []),
179
179
  ...((0, tools_voice_local_1.localAudioToolGate)({ ...context, context: _context }).ok ? [tools_voice_local_1.voiceLocalAudioToolDefinition] : []),