@ouro.bot/cli 0.1.0-alpha.810 → 0.1.0-alpha.812

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 (40) hide show
  1. package/changelog.json +18 -0
  2. package/deploy/unraid/README.txt +3 -6
  3. package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
  4. package/deploy/unraid/sanctuary-deployment-target.mjs +1 -0
  5. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
  6. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  7. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
  8. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  9. package/deploy/unraid/sanctuary.xml +1 -1
  10. package/dist/heart/config.js +32 -2
  11. package/dist/heart/core.js +167 -115
  12. package/dist/heart/daemon/daemon.js +19 -9
  13. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
  14. package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
  15. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
  16. package/dist/heart/frontend-approval-runtime.js +87 -13
  17. package/dist/heart/identity.js +17 -5
  18. package/dist/heart/session-events.js +8 -1
  19. package/dist/heart/tool-approval.js +64 -31
  20. package/dist/mind/pending.js +4 -4
  21. package/dist/repertoire/mcp-manager.js +365 -381
  22. package/dist/repertoire/mcp-tools.js +66 -25
  23. package/dist/repertoire/plugin-mcp.js +3 -3
  24. package/dist/repertoire/shell-sessions.js +8 -7
  25. package/dist/repertoire/tool-arguments.js +30 -8
  26. package/dist/repertoire/tools-session.js +30 -5
  27. package/dist/repertoire/tools-shell.js +30 -14
  28. package/dist/repertoire/tools-voice.js +6 -6
  29. package/dist/repertoire/tools.js +231 -205
  30. package/dist/senses/bluebubbles/index.js +4 -2
  31. package/dist/senses/cli.js +6 -5
  32. package/dist/senses/private-runtime.js +9 -3
  33. package/dist/senses/shared-turn.js +12 -9
  34. package/dist/senses/teams.js +15 -12
  35. package/dist/senses/telegram-approval-runtime.js +129 -20
  36. package/dist/senses/telegram-client.js +19 -2
  37. package/dist/senses/telegram.js +54 -15
  38. package/dist/senses/voice/twilio-phone.js +123 -173
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
package/changelog.json CHANGED
@@ -1,6 +1,24 @@
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.812",
6
+ "changes": [
7
+ "Accept Sanctuary's existing private frontend socket during deployment audits while retaining exact listener and ownership checks."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.811",
12
+ "changes": [
13
+ "Bind tool discovery and execution to the initiating agent with canonical schemas, per-owner MCP connections and current authorization.",
14
+ "Keep tool schema validation isolated so repeated schema IDs work across turns without borrowing another tool's schema registrations.",
15
+ "Expose Sanctuary's resident native tools only to the configured owner while preserving household and event exclusions and role-specific containment.",
16
+ "Match Sanctuary's Unit-16e containment evidence to the canonical three-mount deployment and reject malformed root-mode observations at both the broker and adapter boundaries.",
17
+ "Preserve approval effect outcomes without replay, confine session reads and isolate foreground and background shell ownership.",
18
+ "Keep CLI and realtime voice tool contexts owner-bound across delayed discovery, transport failure and cancellation.",
19
+ "Allow coverage gates to reuse installed Mailbox UI dependencies through an explicit skip-install option without skipping validation."
20
+ ]
21
+ },
4
22
  {
5
23
  "version": "0.1.0-alpha.810",
6
24
  "changes": [
@@ -2787,12 +2787,9 @@ Packaged deployment-target containment gates:
2787
2787
  closed after eight attempts. It always makes bounded exact-ID unpause attempts,
2788
2788
  verifies the same ID and PID resumed their original running, unpaused state,
2789
2789
  and only then performs the final full-profile topology capture. It
2790
- rejects every owned UDP listener and every externally reachable owned TCP listener. Only loopback
2791
- Mailbox port 6876 plus stream-listening endpoints at the fixed daemon and
2792
- acceptance Unix control paths are permitted; all other named Unix endpoint
2793
- types (including datagrams) fail closed. Inherited descriptors for the same
2794
- socket inode are deduplicated, while unknown loopback ports, wildcard/host-address
2795
- listeners, process/socket/listener drift, or ambiguous ownership fail closed.
2790
+ rejects every owned UDP listener and every externally reachable owned TCP listener.
2791
+ Only loopback Mailbox port 6876 plus stream-listening endpoints at /tmp/ouroboros-daemon.sock, /tmp/ouroboros-daemon.sock.frontend, and /home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-control.sock are permitted; all other named Unix endpoint types (including datagrams) fail closed.
2792
+ Inherited descriptors for the same socket inode are deduplicated, while unknown loopback ports, wildcard/host-address listeners, process/socket/listener drift, or ambiguous ownership fail closed.
2796
2793
  Unit 18 uses the separately packaged fixed final command after activation.
2797
2794
  It accepts either the legacy-adoption topology with no canonical rollback or
2798
2795
  exactly one stopped, non-autostarted rollback; running/autostarted rollback,
@@ -70,7 +70,7 @@
70
70
  "restart-attempt-ledger": { "kind": "fixed-ndjson", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/restart-attempts.ndjson", "redaction": "scenario-action-target-digests-and-state-only" },
71
71
  "telegram-turn-receipts": { "kind": "fixed-ndjson", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-turns.ndjson", "redaction": "scenario-update-tool-response-and-delivery-digests-only" },
72
72
  "read-only-denial-receipt": { "kind": "persisted-one-shot-live-graphql-denial", "schemaVersion": "sanctuary-read-only-denial-receipt-v1", "path": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/denial-receipts/<scenario-handle-digest>.json", "attemptPath": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/denial-attempts/<scenario-handle-digest>.json", "endpoint": "http://127.0.0.1/graphql", "targetSnapshotOperation": "denial_target_snapshot", "resumePolicy": "attempted-or-indeterminate-inspect-before-retry", "redaction": "schema-binding-api-classification-counts-restart-count-and-seven-boundary-digests-only" },
73
- "containment-audit": { "kind": "effective-container-key-tool-and-audit-boundary", "schemaVersion": "sanctuary-containment-audit-v1", "path": "/home/ouro/AgentBundles/sanctuary.ouro/tool-profiles.json", "auditPath": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-audit-chain.ndjson", "producers": ["container_snapshot", "inventory_keys", "live-bundle-relationship-capability-registry", "runtime-tool-resolver", "telegram-audit-mac-chain"], "redaction": "counts-static-policy-values-and-sha256-digests-only" },
73
+ "containment-audit": { "kind": "effective-container-key-tool-and-audit-boundary", "schemaVersion": "sanctuary-containment-audit-v2", "path": "/home/ouro/AgentBundles/sanctuary.ouro/tool-profiles.json", "auditPath": "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-audit-chain.ndjson", "producers": ["container_snapshot", "inventory_keys", "live-bundle-relationship-capability-registry", "canonical-turn-selection", "current-relationship-provider-gates", "telegram-audit-mac-chain"], "redaction": "counts-static-policy-values-and-sha256-digests-only" },
74
74
  "container-inspect": { "kind": "fixed-host-snapshot", "path": "/run/ouro-acceptance/container-inspect.json", "redaction": "typed-facts-and-digests-only" },
75
75
  "provider-live-check": { "kind": "fixed-runtime-api", "operation": "sanctuary-provider-readiness", "redaction": "exact same-provider lane selection, one credential revision, live ping attempts, and no-fallback booleans only" },
76
76
  "live-grounding-read": { "kind": "fixed-runtime-api", "operation": "sanctuary-independent-system-or-storage-read", "redaction": "bounded-system-or-storage-facts-and-digest-only" },
@@ -16,6 +16,7 @@ const PROFILES = Object.freeze({
16
16
  })
17
17
  const DOCUMENTED_UNIX_CONTROLS = [
18
18
  "/tmp/ouroboros-daemon.sock",
19
+ "/tmp/ouroboros-daemon.sock.frontend",
19
20
  "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-control.sock",
20
21
  ]
21
22
  const DOCUMENTED_LOOPBACK_TCP_CONTROLS = new Set([6876])
@@ -492,6 +492,7 @@ async function containerSnapshot(expectedImage) {
492
492
  || typeof value.startedAt !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[^\s]{1,64}Z$/u.test(value.startedAt)) {
493
493
  throw new Error("production container identity is invalid")
494
494
  }
495
+ if (typeof value.readOnlyRoot !== "boolean") throw new Error("production container root mode is invalid")
495
496
  const ports = object(value.ports ?? {}, "published ports")
496
497
  const publishedPortCount = Object.values(ports).reduce((count, bindings) => count + (Array.isArray(bindings) ? bindings.length : 0), 0)
497
498
  const mounts = Array.isArray(value.mounts) ? value.mounts.map((raw) => {
@@ -499,14 +500,14 @@ async function containerSnapshot(expectedImage) {
499
500
  return { destination: mount.Destination, source: mount.Source, mode: mount.Mode, propagation: mount.Propagation, rw: mount.RW, type: mount.Type }
500
501
  }).sort((left, right) => String(left.destination).localeCompare(String(right.destination))) : []
501
502
  const expectedMounts = [
502
- { destination: "/home/ouro/.ouro-cli", source: PRODUCTION_RUNTIME_SOURCE, mode: "rw", propagation: "rprivate", rw: true, type: "bind" },
503
- { destination: "/home/ouro/AgentBundles/sanctuary.ouro", source: PRODUCTION_BUNDLE_SOURCE, mode: "rw", propagation: "rprivate", rw: true, type: "bind" },
504
- { destination: "/run/ouro-events", source: PRODUCTION_EVENT_SPOOL_SOURCE, mode: "ro", propagation: "rprivate", rw: false, type: "bind" },
503
+ { destination: "/home/ouro/.ouro-cli", source: PRODUCTION_RUNTIME_SOURCE, propagation: "rprivate", rw: true, type: "bind" },
504
+ { destination: "/home/ouro/AgentBundles/sanctuary.ouro", source: PRODUCTION_BUNDLE_SOURCE, propagation: "rprivate", rw: true, type: "bind" },
505
+ { destination: "/run/ouro-events", source: PRODUCTION_EVENT_SPOOL_SOURCE, propagation: "rprivate", rw: false, type: "bind" },
505
506
  ]
506
- const mountsExact = mounts.length === expectedMounts.length && expectedMounts.every((expected) => mounts.some((mount) => mount.destination === expected.destination && mount.source === expected.source && mount.mode === expected.mode && mount.propagation === expected.propagation && mount.rw === expected.rw && mount.type === expected.type))
507
+ const mountsExact = mounts.length === expectedMounts.length && expectedMounts.every((expected) => mounts.some((mount) => mount.destination === expected.destination && mount.source === expected.source && mount.propagation === expected.propagation && mount.rw === expected.rw && mount.type === expected.type))
507
508
  const securityExact = value.privileged === false && (value.capAdd === null || (Array.isArray(value.capAdd) && value.capAdd.length === 0))
508
- && JSON.stringify(value.capDrop) === JSON.stringify(["ALL"])
509
- && JSON.stringify(value.securityOpt) === JSON.stringify(["no-new-privileges"])
509
+ && (value.capDrop === null || (Array.isArray(value.capDrop) && value.capDrop.length === 0))
510
+ && (value.securityOpt === null || (Array.isArray(value.securityOpt) && value.securityOpt.length === 0))
510
511
  const running = value.running === true
511
512
  if (!running) throw new Error("production container is not running")
512
513
  const configuredUser = text(value.user, "container user")
@@ -534,7 +535,7 @@ async function containerSnapshot(expectedImage) {
534
535
  user: configuredUser,
535
536
  liveProcessUser,
536
537
  processBindingDigest,
537
- readOnlyRoot: value.readOnlyRoot === true,
538
+ readOnlyRoot: value.readOnlyRoot,
538
539
  mountCount: mounts.length,
539
540
  mountsDigest: createHash("sha256").update(JSON.stringify(mounts)).digest("hex"),
540
541
  mountsExact,
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.810",
2
+ "runtimeVersion": "0.1.0-alpha.812",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -2,4 +2,4 @@
2
2
 
3
3
  I am Mendelow Cloud Butler. I care for Ari's home services and help approved household members over Telegram. Ari is my family-trust operator; each other person has their own relationship, permissions, conversation, preferences, and return route.
4
4
 
5
- I live in a non-root container on the Unraid host internally named `sanctuary`. People do not need to say Sanctuary or know which backend serves a household outcome. My primary server interface is the typed Unraid GraphQL repertoire, never shell.
5
+ I live in a non-root container on the Unraid host internally named `sanctuary`. People do not need to say Sanctuary or know which backend serves a household outcome. I use the typed Unraid GraphQL repertoire for server operations and owner-authorized native tools for resident work inside my existing container boundary.
@@ -2,9 +2,9 @@
2
2
  "version": 2,
3
3
  "profiles": {
4
4
  "sanctuary-owner": {
5
- "version": 7,
5
+ "version": 8,
6
6
  "contextScopes": ["household.status", "household.policy", "household.private"],
7
- "toolNames": ["external_event_disposition", "query_active_work", "save_friend_note", "telegram_contact_manage", "query_cares", "care_manage", "await_condition", "resolve_await", "cancel_await", "list_recent_attachments", "materialize_attachment", "describe_image", "unraid_list_containers", "unraid_get_container_logs", "unraid_get_storage", "sanctuary_get_media_optimization", "sanctuary_search_media_catalog", "unraid_get_disks", "unraid_get_notifications", "unraid_get_system", "sanctuary_get_install_state", "unraid_check_services", "sanctuary_get_download_queue", "sanctuary_resume_download_queue", "unraid_restart_container", "steward_policy_manage", "send_message", "ponder", "rest", "settle", "speak"],
7
+ "toolNames": ["external_event_disposition", "query_active_work", "save_friend_note", "telegram_contact_manage", "query_cares", "care_manage", "await_condition", "resolve_await", "cancel_await", "list_recent_attachments", "materialize_attachment", "describe_image", "unraid_list_containers", "unraid_get_container_logs", "unraid_get_storage", "sanctuary_get_media_optimization", "sanctuary_search_media_catalog", "unraid_get_disks", "unraid_get_notifications", "unraid_get_system", "sanctuary_get_install_state", "unraid_check_services", "sanctuary_get_download_queue", "sanctuary_resume_download_queue", "unraid_restart_container", "steward_policy_manage", "send_message", "ponder", "rest", "settle", "speak", "shell", "shell_status", "shell_tail", "read_file", "write_file", "edit_file", "glob", "grep", "web_search", "search_facts", "consult_diary", "consult_notes", "get_friend_note", "session_summary", "query_session", "set_reasoning_effort", "restart_runtime", "revive_sense"],
8
8
  "effectScopes": ["telegram.proactive", "telegram.request_return", "telegram.owner_event"]
9
9
  },
10
10
  "sanctuary-household": {
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.810</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.812</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.InvalidSessionPathError = void 0;
36
37
  exports.loadConfig = loadConfig;
37
38
  exports.resetConfigCache = resetConfigCache;
38
39
  exports.cacheRuntimeConfigForTests = cacheRuntimeConfigForTests;
@@ -59,6 +60,7 @@ exports.getLogsDir = getLogsDir;
59
60
  exports.sanitizeKey = sanitizeKey;
60
61
  exports.canonicalizeTelegramSessionKey = canonicalizeTelegramSessionKey;
61
62
  exports.slugify = slugify;
63
+ exports.isValidSessionCoordinate = isValidSessionCoordinate;
62
64
  exports.resolveSessionPath = resolveSessionPath;
63
65
  exports.sessionPath = sessionPath;
64
66
  exports.logPath = logPath;
@@ -373,12 +375,40 @@ function slugify(value) {
373
375
  .replace(/^-+/, "")
374
376
  .replace(/-+$/, "");
375
377
  }
378
+ class InvalidSessionPathError extends Error {
379
+ }
380
+ exports.InvalidSessionPathError = InvalidSessionPathError;
381
+ function isValidSessionCoordinate(value) {
382
+ return typeof value === "string" && value.length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value);
383
+ }
384
+ function isInsideSessionRoot(root, target) {
385
+ const relative = path.relative(root, target);
386
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
387
+ }
376
388
  function resolveSessionPath(friendId, channel, key, options) {
377
- const dir = path.join((0, identity_1.getAgentRoot)(), "state", "sessions", friendId, channel);
389
+ const agentRoot = options?.agentRoot ?? (0, identity_1.getAgentRoot)();
390
+ if (options?.confined && (!isValidSessionCoordinate(friendId) || !isValidSessionCoordinate(channel)
391
+ || typeof key !== "string" || key.includes("\0") || !path.isAbsolute(agentRoot))) {
392
+ throw new InvalidSessionPathError("invalid session coordinates");
393
+ }
394
+ const sessionsRoot = path.join(agentRoot, "state", "sessions");
395
+ const dir = path.join(sessionsRoot, friendId, channel);
396
+ const file = path.join(dir, sanitizeKey(key) + ".json");
397
+ if (options?.confined) {
398
+ if (!isInsideSessionRoot(sessionsRoot, file))
399
+ throw new InvalidSessionPathError("session path escapes its root");
400
+ if (fs.existsSync(file)) {
401
+ const realSessionsRoot = fs.realpathSync(sessionsRoot);
402
+ if (!isInsideSessionRoot(fs.realpathSync(agentRoot), realSessionsRoot)
403
+ || !isInsideSessionRoot(realSessionsRoot, fs.realpathSync(file))) {
404
+ throw new InvalidSessionPathError("session path escapes the initiating agent");
405
+ }
406
+ }
407
+ }
378
408
  if (options?.ensureDir) {
379
409
  fs.mkdirSync(dir, { recursive: true });
380
410
  }
381
- return path.join(dir, sanitizeKey(key) + ".json");
411
+ return file;
382
412
  }
383
413
  function sessionPath(friendId, channel, key) {
384
414
  return resolveSessionPath(friendId, channel, key, { ensureDir: true });