@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
@@ -65,6 +65,7 @@ const pending_1 = require("../../mind/pending");
65
65
  const agent_service_1 = require("./agent-service");
66
66
  const friends_1 = require("@ouro.bot/friends");
67
67
  const mcp_manager_1 = require("../../repertoire/mcp-manager");
68
+ const mcp_tools_1 = require("../../repertoire/mcp-tools");
68
69
  const mailbox_http_1 = require("../mailbox/mailbox-http");
69
70
  const mailbox_types_1 = require("../mailbox/mailbox-types");
70
71
  const mailbox_read_1 = require("../mailbox/mailbox-read");
@@ -1161,7 +1162,6 @@ class OuroDaemon {
1161
1162
  }
1162
1163
  this.frontendSessionService.close?.();
1163
1164
  (0, update_checker_1.stopUpdateChecker)();
1164
- (0, mcp_manager_1.shutdownSharedMcpManager)();
1165
1165
  this.scheduler.stop?.();
1166
1166
  this.healthMonitor.stopPeriodicChecks?.();
1167
1167
  if (this.senseAutostartTimer) {
@@ -1172,7 +1172,10 @@ class OuroDaemon {
1172
1172
  clearInterval(this.externalEventReconcileTimer);
1173
1173
  this.externalEventReconcileTimer = null;
1174
1174
  }
1175
- const workerStopTasks = [Promise.resolve().then(() => this.processManager.stopAll())];
1175
+ const workerStopTasks = [
1176
+ Promise.resolve().then(() => this.processManager.stopAll()),
1177
+ Promise.resolve().then(() => (0, mcp_manager_1.shutdownSharedMcpManager)()),
1178
+ ];
1176
1179
  if (this.senseManager) {
1177
1180
  workerStopTasks.push(Promise.resolve().then(() => this.senseManager.stopAll()));
1178
1181
  }
@@ -2446,28 +2449,35 @@ class OuroDaemon {
2446
2449
  }
2447
2450
  case "mcp.list": {
2448
2451
  return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
2449
- (0, identity_1.setAgentName)(command.agent ?? "default");
2450
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
2452
+ const agentName = command.agent ?? "default";
2453
+ const owner = { agentName, agentRoot: (0, identity_1.getAgentRoot)(agentName) };
2454
+ (0, identity_1.setAgentName)(agentName);
2455
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(owner);
2451
2456
  if (!mcpManager) {
2452
2457
  return { ok: true, data: [], message: "no MCP servers configured" };
2453
2458
  }
2454
- return { ok: true, data: mcpManager.listAllTools() };
2459
+ return { ok: true, data: mcpManager.entries.map(({ server, tools, pluginId }) => ({ server, tools, pluginId })) };
2455
2460
  });
2456
2461
  }
2457
2462
  case "mcp.call": {
2458
2463
  return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
2459
- (0, identity_1.setAgentName)(command.agent ?? "default");
2460
- const mcpCallManager = await (0, mcp_manager_1.getSharedMcpManager)();
2464
+ const agentName = command.agent ?? "default";
2465
+ const owner = { agentName, agentRoot: (0, identity_1.getAgentRoot)(agentName) };
2466
+ (0, identity_1.setAgentName)(agentName);
2467
+ const mcpCallManager = await (0, mcp_manager_1.getSharedMcpManager)(owner);
2461
2468
  if (!mcpCallManager) {
2462
2469
  return { ok: false, error: "no MCP servers configured" };
2463
2470
  }
2464
2471
  try {
2465
2472
  const parsedArgs = command.args ? JSON.parse(command.args) : {};
2466
- const result = await mcpCallManager.callTool(command.server, command.tool, parsedArgs);
2473
+ const matches = (0, mcp_tools_1.mcpToolsAsDefinitions)(mcpCallManager).filter((definition) => definition.mcpBinding?.server === command.server && definition.mcpBinding.rawName === command.tool);
2474
+ const binding = matches.length === 1 ? matches[0].mcpBinding : undefined;
2475
+ if (!binding)
2476
+ return { ok: false, error: "MCP tool is unavailable or ambiguous" };
2477
+ const result = await mcpCallManager.manager.callTool(binding, parsedArgs, owner);
2467
2478
  return { ok: true, data: result };
2468
2479
  }
2469
2480
  catch (error) {
2470
- /* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
2471
2481
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
2472
2482
  }
2473
2483
  });
@@ -60,6 +60,7 @@ const node_crypto_1 = require("node:crypto");
60
60
  const node_fs_1 = require("node:fs");
61
61
  const node_net_1 = require("node:net");
62
62
  const path = __importStar(require("node:path"));
63
+ const node_util_1 = require("node:util");
63
64
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
64
65
  const friends_1 = require("@ouro.bot/friends");
65
66
  const runtime_1 = require("../../nerves/runtime");
@@ -258,6 +259,7 @@ function createSanctuaryAcceptanceAdapterDependencies(secretFd = 3, options = {}
258
259
  createTelegramApi: telegram_client_1.createTelegramBotApi,
259
260
  readLiveGrounding: readIndependentSanctuaryGrounding,
260
261
  runProductionBoundaryProbe: runSanctuaryProductionBoundaryProbe,
262
+ providerRuntime: core_1.getProviderRuntime,
261
263
  };
262
264
  const healthDriver = createSanctuaryHealthAcceptanceScenarioDriver(dependencies.hostRequest);
263
265
  const scenarioAgentRoot = options.scenarioCapture?.agentRoot ?? (0, identity_1.getAgentRoot)(TARGET_ID);
@@ -937,32 +939,57 @@ function canonicalDockerIdFromUnraidPrefixedId(value) {
937
939
  throw new Error("Unraid Docker PrefixedID is invalid");
938
940
  return value.slice(65);
939
941
  }
940
- async function runSanctuaryProductionBoundaryProbe(telegramSchemas) {
941
- const excludedNames = ["shell", "read_file", "edit_file", "vault_get", "mcp_call", "exec", "credential_get"];
942
+ function containmentToolContext(agentRoot, profileId) {
943
+ const profile = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot).profiles[profileId];
944
+ if (!profile)
945
+ throw new Error("Sanctuary containment profile is missing");
946
+ return {
947
+ signin: async () => undefined, agentName: TARGET_ID, agentRoot,
948
+ relationshipAuthorization: {
949
+ profileId, authorizedContextScopes: profile.contextScopes, advertisedToolNames: profile.toolNames,
950
+ // This request-bound subject audits the profile contract; it is not a persisted Friend or grant.
951
+ authorizeTool: async (name) => (0, relationship_authorization_1.authorizeRelationshipAccess)({
952
+ relationship: { friendId: "sanctuary-containment-probe", trustLevel: profileId === "sanctuary-household" ? "friend" : "family", admissionState: "active", initiativePolicy: "reactive_only", capabilityProfileId: profileId },
953
+ profiles: Object.values((0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot).profiles),
954
+ request: { kind: "tool", name, requestId: "sanctuary-containment-probe", returnTargetFriendId: "sanctuary-containment-probe" },
955
+ activeRequestId: "sanctuary-containment-probe", requestPhase: "inbound",
956
+ }),
957
+ },
958
+ };
959
+ }
960
+ async function runSanctuaryProductionBoundaryProbe(input) {
961
+ const excludedNames = ["vault_get", "mcp_call", "exec", "credential_get", ...(input.profileId === "sanctuary-owner" ? [] : tools_1.SANCTUARY_OWNER_ADDITIONS)];
962
+ const inner = input.profileId === "sanctuary-event";
963
+ const terminal = inner ? "rest" : "settle";
942
964
  const turns = [
943
965
  { content: "", toolCalls: excludedNames.map((name, index) => ({ id: `sanctuary-excluded-${index}`, name, arguments: "{}" })), outputItems: [] },
944
966
  { content: "", toolCalls: [{ id: "sanctuary-valid-system", name: "unraid_get_system", arguments: "{}" }], outputItems: [] },
945
- { content: "", toolCalls: [{ id: "sanctuary-boundary-settle", name: "settle", arguments: JSON.stringify({ answer: "boundary complete" }) }], outputItems: [] },
967
+ { content: "", toolCalls: [{ id: "sanctuary-boundary-terminal", name: terminal, arguments: JSON.stringify(inner ? { note: "boundary complete" } : { answer: "boundary complete", intent: "complete" }) }], outputItems: [] },
946
968
  ];
947
969
  let turn = 0;
948
970
  const controlOutputs = [];
949
971
  const providerRuntime = {
950
- id: "minimax", model: "sanctuary-production-boundary-probe", client: null, capabilities: new Set(),
972
+ ...input.providerRuntime, client: null,
951
973
  streamTurn: async () => turns[turn++] ?? (() => { throw new Error("production boundary probe exceeded its turn budget"); })(),
952
974
  appendToolOutput: (callId, output) => { if (callId === "sanctuary-valid-system")
953
975
  controlOutputs.push(output); }, resetTurnState: () => undefined, ping: async () => undefined, classifyError: () => "unknown",
954
976
  };
955
977
  const receipts = [];
956
978
  const sanctuary = (0, sanctuary_runtime_1.createSanctuaryToolContext)(TARGET_ID).sanctuary;
979
+ const context = containmentToolContext(input.agentRoot, input.profileId);
980
+ const relationship = context.relationshipAuthorization;
981
+ const poisonedContext = { ...context, sanctuary, relationshipAuthorization: {
982
+ ...relationship, advertisedToolNames: [...relationship.advertisedToolNames, ...excludedNames, "mcp__containment_poison"],
983
+ } };
957
984
  const observed = await (0, sanctuary_runtime_1.runWithSanctuaryToolReceiptCollection)(() => (0, core_1.runAgent)([{ role: "user", content: "Run the bounded production tool authorization probe." }], {
958
985
  onModelStart: () => undefined, onModelStreamStart: () => undefined, onTextChunk: () => undefined, onReasoningChunk: () => undefined,
959
986
  onToolStart: () => undefined, onToolEnd: () => undefined, onError: () => undefined, onClearText: () => undefined,
960
- }, "telegram", undefined, {
961
- tools: telegramSchemas, providerRuntimeOverride: providerRuntime, toolBoundaryObserver: (receipt) => receipts.push(receipt),
962
- toolContext: { signin: async () => undefined, sanctuary },
987
+ }, inner ? "inner" : "telegram", undefined, {
988
+ skipKeptNotes: true, providerRuntimeOverride: providerRuntime, toolBoundaryObserver: (receipt) => receipts.push(receipt),
989
+ toolContext: poisonedContext,
963
990
  }));
964
991
  const controlReceipts = receipts.filter((receipt) => receipt.name === "unraid_get_system");
965
- if (observed.result.outcome !== "settled" || controlReceipts.length !== 1
992
+ if (observed.result.outcome !== (inner ? "rested" : "settled") || controlReceipts.length !== 1
966
993
  || controlReceipts[0].reason !== "dispatched" || !controlReceipts[0].invoked || controlReceipts[0].sideEffect
967
994
  || observed.toolResultDigests.length !== 1 || controlOutputs.length !== 1
968
995
  || (0, node_crypto_1.createHash)("sha256").update(controlOutputs[0]).digest("hex") !== observed.toolResultDigests[0]) {
@@ -973,7 +1000,7 @@ async function runSanctuaryProductionBoundaryProbe(telegramSchemas) {
973
1000
  if (controlResult.ok !== true || typeof controlData.sourceIdentityDigest !== "string" || !SHA256.test(controlData.sourceIdentityDigest)) {
974
1001
  throw new Error("production boundary valid control result is invalid");
975
1002
  }
976
- return receipts.filter((receipt) => receipt.name !== "settle");
1003
+ return receipts.filter((receipt) => receipt.name !== terminal);
977
1004
  }
978
1005
  function parseInteractiveDriverReceipt(raw, label, scenarioHandleDigest) {
979
1006
  if (raw === null)
@@ -1505,26 +1532,52 @@ async function readDefaultSanctuaryScenarioFacts(label, scenarioHandleDigest, de
1505
1532
  })).sort((left, right) => left.name.localeCompare(right.name));
1506
1533
  const readRecord = inventory.find((record) => record.name === "Butler RO");
1507
1534
  const writeRecord = inventory.find((record) => record.name === "Butler RW");
1535
+ const rawProfiles = (0, node_fs_1.readFileSync)(path.join(agentRoot, "tool-profiles.json"), "utf8");
1536
+ const packagedProfiles = (0, node_fs_1.readFileSync)(path.resolve(__dirname, "../../../deploy/unraid/sanctuary.ouro/tool-profiles.json"), "utf8");
1537
+ if (!(0, node_util_1.isDeepStrictEqual)(JSON.parse(rawProfiles), JSON.parse(packagedProfiles)))
1538
+ throw new Error("Sanctuary containment profiles do not match the verified package");
1508
1539
  const relationshipRegistry = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot);
1509
- const telegramProfile = relationshipRegistry.profiles["sanctuary-owner"];
1510
- const privateProfile = relationshipRegistry.profiles["sanctuary-event"];
1511
- const telegramNames = telegramProfile?.toolNames ?? [];
1512
- const privateNames = privateProfile?.toolNames ?? [];
1513
- const relationshipProfilesExact = telegramNames.length > 0 && privateNames.length > 0;
1514
- const flowSchemas = new Map([tools_1.ponderTool, tools_1.settleTool, tools_1.speakTool, tools_1.restTool].map((tool) => [tool.function.name, tool]));
1515
- const schemasFor = (names) => names.flatMap((name) => {
1516
- const schema = (0, tools_1.resolveToolDefinition)(name)?.tool ?? flowSchemas.get(name);
1517
- return schema ? [schema] : [];
1518
- });
1519
- const telegramSchemas = schemasFor(telegramNames);
1520
- const privateSchemas = schemasFor(privateNames);
1521
- const handlerResolves = (name) => typeof (0, tools_1.resolveToolDefinition)(name)?.handler === "function" || flowSchemas.has(name);
1522
- const resolvedHandlerCount = [...telegramNames, ...privateNames].filter(handlerResolves).length;
1523
- const handlersExact = resolvedHandlerCount === telegramNames.length + privateNames.length;
1524
- const excludedNames = ["shell", "read_file", "edit_file", "vault_get", "mcp_call", "exec", "credential_get"];
1525
- const excludedSchemaIntersection = excludedNames.filter((name) => telegramNames.includes(name) || privateNames.includes(name));
1526
- const productionBoundaryReceipts = await dependency(deps.runProductionBoundaryProbe, "production tool boundary probe")(telegramSchemas);
1527
- const excludedAttempts = productionBoundaryReceipts.filter((receipt) => excludedNames.includes(receipt.name));
1540
+ const digest = (value) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
1541
+ const boundaryFor = async (profileId) => {
1542
+ const profile = relationshipRegistry.profiles[profileId];
1543
+ const inner = profileId === "sanctuary-event";
1544
+ const runtime = await dependency(deps.providerRuntime, "current provider runtime")(inner ? "agent" : "human", { agentName: TARGET_ID, agentRoot });
1545
+ const context = containmentToolContext(agentRoot, profileId);
1546
+ const select = (toolContext) => (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)(inner ? "inner" : "telegram"), undefined, undefined, runtime.capabilities, undefined, undefined, toolContext);
1547
+ const selection = select(context);
1548
+ const schemas = (0, tools_1.toolSelectionSchemas)(selection);
1549
+ const schemaToolNames = schemas.map((tool) => tool.function.name);
1550
+ const expectedNames = profile.toolNames.filter((name) => (inner || name !== "rest") && (name !== "set_reasoning_effort" || runtime.capabilities.has("reasoning-effort")));
1551
+ const excludedNames = ["vault_get", "mcp_call", "exec", "credential_get", ...(profileId === "sanctuary-owner" ? [] : tools_1.SANCTUARY_OWNER_ADDITIONS)];
1552
+ const poisoned = select({ ...context, relationshipAuthorization: {
1553
+ ...context.relationshipAuthorization, advertisedToolNames: [...profile.toolNames, ...excludedNames, "mcp__containment_poison"],
1554
+ } });
1555
+ const productionBoundaryReceipts = await dependency(deps.runProductionBoundaryProbe, "production tool boundary probe")({ agentRoot, profileId, providerRuntime: runtime });
1556
+ const excludedAttempts = productionBoundaryReceipts.filter((receipt) => receipt.name !== "unraid_get_system");
1557
+ return {
1558
+ profileId, profileVersion: profile.version, profileDigest: digest(profile), profileToolNames: profile.toolNames,
1559
+ providerCapabilities: [...runtime.capabilities], schemaDigest: digest(schemas), schemaToolNames,
1560
+ ordinaryDefinitionCount: selection.ordinary.length, engineSchemaCount: selection.engine.length,
1561
+ profileExact: true, schemasExact: (0, node_util_1.isDeepStrictEqual)([...schemaToolNames].sort(), [...expectedNames].sort()),
1562
+ handlersExact: selection.ordinary.every((definition) => typeof definition.handler === "function") && selection.engine.every((schema) => [tools_1.ponderTool, tools_1.settleTool, tools_1.speakTool, tools_1.restTool].includes(schema)),
1563
+ poisonedSchemaIntersectionCount: (0, tools_1.toolSelectionSchemas)(poisoned).filter((schema) => !schemaToolNames.includes(schema.function.name)).length,
1564
+ excludedToolNames: excludedAttempts.map((receipt) => receipt.name),
1565
+ excludedSchemaIntersectionCount: schemaToolNames.filter((name) => excludedNames.includes(name)).length,
1566
+ fabricatedHandlerInvocationCount: excludedAttempts.filter((receipt) => receipt.invoked).length,
1567
+ excludedToolAttemptCount: excludedAttempts.length,
1568
+ excludedToolRejectedCount: excludedAttempts.filter((receipt) => receipt.reason === "profile_excluded").length,
1569
+ excludedToolInvokedCount: excludedAttempts.filter((receipt) => receipt.invoked).length,
1570
+ excludedToolSideEffectCount: excludedAttempts.filter((receipt) => receipt.sideEffect).length,
1571
+ globallyResolvableExcludedToolCount: excludedAttempts.filter((receipt) => receipt.globallyResolvable).length,
1572
+ };
1573
+ };
1574
+ const profileBoundaries = {
1575
+ "sanctuary-owner": await boundaryFor("sanctuary-owner"),
1576
+ "sanctuary-household": await boundaryFor("sanctuary-household"),
1577
+ "sanctuary-event": await boundaryFor("sanctuary-event"),
1578
+ };
1579
+ if ((0, node_fs_1.readFileSync)(path.join(agentRoot, "tool-profiles.json"), "utf8") !== rawProfiles)
1580
+ throw new Error("Sanctuary containment profiles changed during the audit");
1528
1581
  const restartDefinition = (0, tools_1.resolveToolDefinition)("unraid_restart_container");
1529
1582
  const writeApprovalPolicy = restartDefinition.approvalPolicy({ container: "calibre-web" });
1530
1583
  const writeApprovalPolicyExact = writeApprovalPolicy.kind === "required"
@@ -1543,30 +1596,16 @@ async function readDefaultSanctuaryScenarioFacts(label, scenarioHandleDigest, de
1543
1596
  }
1544
1597
  const rawWriteMaterialFieldCount = rawInventory.reduce((count, record) => count
1545
1598
  + Object.keys(record).filter((field) => /^(?:key|credential|secret|token)$/iu.test(field)).length, 0);
1599
+ if (container && typeof container.readOnlyRoot !== "boolean")
1600
+ throw new Error("containment root-mode observation must be boolean");
1546
1601
  containment = {
1547
- schemaVersion: "sanctuary-containment-audit-v1",
1602
+ schemaVersion: "sanctuary-containment-audit-v2",
1548
1603
  keyCount: inventory.length,
1549
1604
  keyInventoryDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(redactedInventory)).digest("hex"),
1550
1605
  readScopeDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(readRecord ? flattenedPermissions(readRecord) : [])).digest("hex"),
1551
1606
  writeScopeDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(writeRecord ? flattenedPermissions(writeRecord) : [])).digest("hex"),
1552
1607
  keyRoleAssignmentCount: inventory.reduce((count, record) => count + record.roles.length, 0),
1553
- telegramToolCount: telegramNames.length,
1554
- telegramProfileDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(telegramNames)).digest("hex"),
1555
- telegramSchemaDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(telegramSchemas)).digest("hex"),
1556
- privateToolCount: privateNames.length,
1557
- privateProfileDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(privateNames)).digest("hex"),
1558
- privateSchemaDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(privateSchemas)).digest("hex"),
1559
- resolvedHandlerCount,
1560
- relationshipProfilesExact,
1561
- handlersExact,
1562
- excludedToolCount: excludedNames.length,
1563
- excludedSchemaIntersectionCount: excludedSchemaIntersection.length,
1564
- fabricatedHandlerInvocationCount: excludedSchemaIntersection.filter(handlerResolves).length,
1565
- excludedToolAttemptCount: excludedAttempts.length,
1566
- excludedToolRejectedCount: excludedAttempts.filter((attempt) => attempt.reason === "profile_excluded").length,
1567
- excludedToolInvokedCount: excludedAttempts.filter((attempt) => attempt.invoked).length,
1568
- excludedToolSideEffectCount: excludedAttempts.filter((attempt) => attempt.sideEffect).length,
1569
- globallyResolvableExcludedToolCount: excludedAttempts.filter((attempt) => attempt.globallyResolvable).length,
1608
+ profileBoundaries,
1570
1609
  auditPathDigest: (0, node_crypto_1.createHash)("sha256").update(TELEGRAM_AUDIT).digest("hex"),
1571
1610
  auditLedgerDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(auditLedgerEntries)).digest("hex"),
1572
1611
  auditRecordCount: auditLedgerEntries.length,
@@ -1582,7 +1621,7 @@ async function readDefaultSanctuaryScenarioFacts(label, scenarioHandleDigest, de
1582
1621
  updaterDisabled: container?.updaterDisabled === true,
1583
1622
  writableKeyExposure: container?.writableKeyExposure !== false,
1584
1623
  rawWriteMaterialFieldCount,
1585
- typedWriteExecutorCount: telegramNames.filter((name) => name === "unraid_restart_container" && writeApprovalPolicy.kind === "required").length,
1624
+ typedWriteExecutorCount: relationshipRegistry.profiles["sanctuary-owner"].toolNames.filter((name) => name === "unraid_restart_container" && writeApprovalPolicy.kind === "required").length,
1586
1625
  writeApprovalPolicyDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(writeApprovalPolicy)).digest("hex"),
1587
1626
  writeApprovalPolicyExact,
1588
1627
  sensitiveMaterialObserved: auditContainsSensitiveMaterial(auditRaw ?? "", deps.telegramCredentials?.()) || rawWriteMaterialFieldCount > 0 || container?.writableKeyExposure === true,
@@ -1652,9 +1691,8 @@ async function readDefaultSanctuaryScenarioFacts(label, scenarioHandleDigest, de
1652
1691
  digest: health && digestFiredWithinMs !== null ? { scheduleObserved: Boolean(cronRaw && canonicalSanctuaryHealthCronRegistered(cronRaw)), messageCount: scenarioDeliveries.filter((receipt) => receipt.kind === "digest" || receipt.kind === "transition_and_digest").length, firedWithinMs: digestFiredWithinMs, productionRestored: container?.running === true && container.health === "healthy" } : undefined,
1653
1692
  reboot,
1654
1693
  containment: containment ?? {
1655
- schemaVersion: "sanctuary-containment-audit-v1", keyCount: 0, keyInventoryDigest: "", readScopeDigest: "", writeScopeDigest: "", keyRoleAssignmentCount: 0,
1656
- telegramToolCount: 0, telegramProfileDigest: "", telegramSchemaDigest: "", privateToolCount: 0, privateProfileDigest: "", privateSchemaDigest: "", resolvedHandlerCount: 0, relationshipProfilesExact: false, handlersExact: false,
1657
- excludedToolCount: 0, excludedSchemaIntersectionCount: 0, fabricatedHandlerInvocationCount: 0, excludedToolAttemptCount: 0, excludedToolRejectedCount: 0, excludedToolInvokedCount: 0, excludedToolSideEffectCount: 0, globallyResolvableExcludedToolCount: 0, auditPathDigest: "", auditLedgerDigest: "", auditRecordCount: 0, auditLifecyclePairCount: 0,
1694
+ schemaVersion: "sanctuary-containment-audit-v2", keyCount: 0, keyInventoryDigest: "", readScopeDigest: "", writeScopeDigest: "", keyRoleAssignmentCount: 0,
1695
+ profileBoundaries: null, auditPathDigest: "", auditLedgerDigest: "", auditRecordCount: 0, auditLifecyclePairCount: 0,
1658
1696
  containerUser: "", liveProcessUser: "", mountCount: 0, publishedPortCount: 0, networkMode: "", readOnlyRoot: false, mountsExact: false, securityExact: false, updaterDisabled: false,
1659
1697
  writableKeyExposure: container?.writableKeyExposure === true, rawWriteMaterialFieldCount: 0, typedWriteExecutorCount: 0, writeApprovalPolicyDigest: "", writeApprovalPolicyExact: false,
1660
1698
  sensitiveMaterialObserved: auditContainsSensitiveMaterial(auditRaw ?? "") || container?.writableKeyExposure === true,
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SANCTUARY_SCENARIO_SOURCES = exports.SANCTUARY_SCENARIO_GATES = exports.SANCTUARY_UNIT_16_EVIDENCE_LABELS = void 0;
37
+ exports.exactSanctuaryContainmentProfileBoundaries = exactSanctuaryContainmentProfileBoundaries;
37
38
  exports.resolveSanctuaryAdapterTimeoutMs = resolveSanctuaryAdapterTimeoutMs;
38
39
  exports.createSanctuaryAcceptanceHarnessDependencies = createSanctuaryAcceptanceHarnessDependencies;
39
40
  exports.sanctuaryScenarioTimeoutBudget = sanctuaryScenarioTimeoutBudget;
@@ -44,14 +45,57 @@ const node_child_process_1 = require("node:child_process");
44
45
  const node_fs_1 = require("node:fs");
45
46
  const node_fs_2 = require("node:fs");
46
47
  const path = __importStar(require("node:path"));
48
+ const node_util_1 = require("node:util");
49
+ const friends_1 = require("@ouro.bot/friends");
47
50
  const runtime_1 = require("../../nerves/runtime");
48
51
  const telegram_1 = require("../../senses/telegram");
49
52
  const runtime_credentials_1 = require("../runtime-credentials");
53
+ const relationship_authorization_1 = require("../../repertoire/relationship-authorization");
54
+ const tools_1 = require("../../repertoire/tools");
50
55
  const MAX_ADAPTER_OUTPUT = 1_048_576;
51
56
  const DEFAULT_ADAPTER_TIMEOUT_MS = 240_000;
52
57
  const DEFAULT_TELEGRAM_TIMEOUT_MS = 10_000;
53
58
  const PACKAGED_PROVENANCE_ADAPTER = "/opt/ouro/deploy/unraid/sanctuary-acceptance-adapter.sh";
54
59
  const OPAQUE_DIGEST = /^[0-9a-f]{64}$/u;
60
+ function exactSanctuaryContainmentProfileBoundaries(value) {
61
+ if (!value || typeof value !== "object" || Array.isArray(value))
62
+ return false;
63
+ const boundaries = value;
64
+ const versions = { "sanctuary-owner": 8, "sanctuary-household": 5, "sanctuary-event": 4 };
65
+ if (!(0, node_util_1.isDeepStrictEqual)(Object.keys(boundaries).sort(), Object.keys(versions).sort()))
66
+ return false;
67
+ const packageRoot = path.resolve(__dirname, "../../../deploy/unraid/sanctuary.ouro");
68
+ const registry = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(packageRoot);
69
+ const digest = (input) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(input)).digest("hex");
70
+ return Object.entries(versions).every(([id, version]) => {
71
+ const raw = boundaries[id];
72
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
73
+ return false;
74
+ const boundary = raw;
75
+ const capabilities = boundary.providerCapabilities;
76
+ if (!Array.isArray(capabilities) || !capabilities.every((entry) => entry === "reasoning-effort" || entry === "phase-annotation")
77
+ || new Set(capabilities).size !== capabilities.length)
78
+ return false;
79
+ const profile = registry.profiles[id];
80
+ if (profile.version !== version)
81
+ return false;
82
+ const selection = (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)(id === "sanctuary-event" ? "inner" : "telegram"), undefined, undefined, new Set(capabilities), undefined, undefined, {
83
+ agentName: "sanctuary", relationshipAuthorization: { profileId: id, advertisedToolNames: profile.toolNames },
84
+ });
85
+ const schemas = (0, tools_1.toolSelectionSchemas)(selection);
86
+ const excludedToolNames = ["vault_get", "mcp_call", "exec", "credential_get", ...(id === "sanctuary-owner" ? [] : tools_1.SANCTUARY_OWNER_ADDITIONS)];
87
+ const globallyResolvableExcludedToolCount = excludedToolNames.filter((name) => (0, tools_1.resolveToolDefinition)(name)).length;
88
+ return globallyResolvableExcludedToolCount > 0 && (0, node_util_1.isDeepStrictEqual)(boundary, {
89
+ profileId: id, profileVersion: version, profileDigest: digest(profile), profileToolNames: profile.toolNames,
90
+ providerCapabilities: capabilities, schemaDigest: digest(schemas), schemaToolNames: schemas.map((tool) => tool.function.name),
91
+ ordinaryDefinitionCount: selection.ordinary.length, engineSchemaCount: selection.engine.length,
92
+ profileExact: true, schemasExact: true, handlersExact: true, poisonedSchemaIntersectionCount: 0,
93
+ excludedToolNames, excludedSchemaIntersectionCount: 0, fabricatedHandlerInvocationCount: 0,
94
+ excludedToolAttemptCount: excludedToolNames.length, excludedToolRejectedCount: excludedToolNames.length,
95
+ excludedToolInvokedCount: 0, excludedToolSideEffectCount: 0, globallyResolvableExcludedToolCount,
96
+ });
97
+ });
98
+ }
55
99
  function resolveSanctuaryAdapterTimeoutMs(configured, remaining) {
56
100
  const maximum = configured ?? DEFAULT_ADAPTER_TIMEOUT_MS;
57
101
  return Math.max(1, Math.min(maximum, remaining ?? maximum));
@@ -494,15 +538,14 @@ function validateSanctuaryUnit16EvidenceAssertions(label, raw) {
494
538
  case "unit-16e-containment-audit":
495
539
  exact([
496
540
  "schemaVersion", "keyCount", "keyInventoryDigest", "readScopeDigest", "writeScopeDigest", "keyRoleAssignmentCount",
497
- "telegramToolCount", "telegramProfileDigest", "telegramSchemaDigest", "privateToolCount", "privateProfileDigest", "privateSchemaDigest", "resolvedHandlerCount", "relationshipProfilesExact", "handlersExact",
498
- "excludedToolCount", "excludedSchemaIntersectionCount", "fabricatedHandlerInvocationCount", "excludedToolAttemptCount", "excludedToolRejectedCount", "excludedToolInvokedCount", "excludedToolSideEffectCount", "globallyResolvableExcludedToolCount",
541
+ "profileBoundaries",
499
542
  "auditPathDigest", "auditLedgerDigest", "auditRecordCount", "auditLifecyclePairCount",
500
543
  "containerUser", "liveProcessUser", "mountCount", "publishedPortCount", "networkMode", "readOnlyRoot", "mountsExact", "securityExact", "updaterDisabled", "writableKeyExposure",
501
544
  "rawWriteMaterialFieldCount", "typedWriteExecutorCount", "writeApprovalPolicyDigest", "writeApprovalPolicyExact", "sensitiveMaterialObserved", "mutationCount",
502
545
  ]);
503
- if (text(value.schemaVersion, `${label} schemaVersion`) !== "sanctuary-containment-audit-v1")
546
+ if (text(value.schemaVersion, `${label} schemaVersion`) !== "sanctuary-containment-audit-v2")
504
547
  throw new Error(`${label} schemaVersion is invalid`);
505
- for (const key of ["keyInventoryDigest", "readScopeDigest", "writeScopeDigest", "telegramProfileDigest", "telegramSchemaDigest", "privateProfileDigest", "privateSchemaDigest", "auditPathDigest", "auditLedgerDigest", "writeApprovalPolicyDigest"])
548
+ for (const key of ["keyInventoryDigest", "readScopeDigest", "writeScopeDigest", "auditPathDigest", "auditLedgerDigest", "writeApprovalPolicyDigest"])
506
549
  opaqueDigest(value[key], `${label} ${key}`);
507
550
  for (const [key, expected] of Object.entries({
508
551
  readScopeDigest: "9914469afdcb574937d1020a03faa82e3c02d767169d3eccae4b81863dafa06e",
@@ -513,24 +556,19 @@ function validateSanctuaryUnit16EvidenceAssertions(label, raw) {
513
556
  throw new Error(`${label} ${key} does not match the canonical contract`);
514
557
  requiredInteger(value, "keyCount", 2, label);
515
558
  requiredInteger(value, "keyRoleAssignmentCount", 0, label);
516
- const telegramToolCount = integer(value.telegramToolCount, `${label} telegramToolCount`, 1);
517
- const privateToolCount = integer(value.privateToolCount, `${label} privateToolCount`, 1);
518
- if (integer(value.resolvedHandlerCount, `${label} resolvedHandlerCount`, 1) !== telegramToolCount + privateToolCount)
519
- throw new Error(`${label} handler count does not match the live relationship profiles`);
520
- requiredInteger(value, "excludedToolCount", 7, label);
521
- requiredInteger(value, "excludedToolAttemptCount", 7, label);
522
- requiredInteger(value, "excludedToolRejectedCount", 7, label);
523
- integer(value.globallyResolvableExcludedToolCount, `${label} globallyResolvableExcludedToolCount`, 1);
524
- allZero(["excludedSchemaIntersectionCount", "fabricatedHandlerInvocationCount", "excludedToolInvokedCount", "excludedToolSideEffectCount", "publishedPortCount", "rawWriteMaterialFieldCount", "mutationCount"]);
559
+ if (!exactSanctuaryContainmentProfileBoundaries(value.profileBoundaries))
560
+ throw new Error(`${label} profileBoundaries do not match the canonical contract`);
561
+ allZero(["publishedPortCount", "rawWriteMaterialFieldCount", "mutationCount"]);
525
562
  requiredFalse(value, "sensitiveMaterialObserved", label);
526
563
  requiredFalse(value, "writableKeyExposure", label);
564
+ requiredFalse(value, "readOnlyRoot", label);
527
565
  integer(value.auditRecordCount, `${label} auditRecordCount`, 2);
528
566
  integer(value.auditLifecyclePairCount, `${label} auditLifecyclePairCount`, 1);
529
567
  if (text(value.containerUser, `${label} containerUser`) !== "10001:10001" || text(value.liveProcessUser, `${label} liveProcessUser`) !== "10001:10001" || text(value.networkMode, `${label} networkMode`) !== "host")
530
568
  throw new Error(`${label} container identity or network is invalid`);
531
- requiredInteger(value, "mountCount", 4, label);
569
+ requiredInteger(value, "mountCount", 3, label);
532
570
  requiredInteger(value, "typedWriteExecutorCount", 1, label);
533
- allTrue(["relationshipProfilesExact", "handlersExact", "writeApprovalPolicyExact", "readOnlyRoot", "mountsExact", "securityExact", "updaterDisabled"]);
571
+ allTrue(["writeApprovalPolicyExact", "mountsExact", "securityExact", "updaterDisabled"]);
534
572
  break;
535
573
  case "unit-16e-1-stop-denial":
536
574
  case "unit-16e-2-restart-denial":
@@ -121,30 +121,19 @@ function hash(value) {
121
121
  }
122
122
  const CONTAINMENT_READ_SCOPE = ["ARRAY", "DASHBOARD", "DISK", "DOCKER", "INFO", "LOGS", "NOTIFICATIONS", "SHARE", "VARS"]
123
123
  .map((resource) => `${resource}:READ_ANY`).sort();
124
- const CONTAINMENT_EXCLUDED_TOOLS = ["shell", "read_file", "edit_file", "vault_get", "mcp_call", "exec", "credential_get"];
125
124
  const CONTAINMENT_AUDIT_PATH = "/home/ouro/AgentBundles/sanctuary.ouro/state/acceptance/telegram-audit-chain.ndjson";
126
125
  function exactContainmentAudit(evidence) {
127
- const digestFields = [evidence.keyInventoryDigest, evidence.telegramProfileDigest, evidence.telegramSchemaDigest, evidence.privateProfileDigest, evidence.privateSchemaDigest, evidence.auditLedgerDigest, evidence.writeApprovalPolicyDigest];
128
- return evidence.schemaVersion === "sanctuary-containment-audit-v1"
126
+ const digestFields = [evidence.keyInventoryDigest, evidence.auditLedgerDigest, evidence.writeApprovalPolicyDigest];
127
+ return evidence.schemaVersion === "sanctuary-containment-audit-v2"
129
128
  && evidence.keyCount === 2
130
129
  && evidence.readScopeDigest === hash(CONTAINMENT_READ_SCOPE)
131
130
  && evidence.writeScopeDigest === hash([...CONTAINMENT_READ_SCOPE, "DOCKER:UPDATE_ANY"].sort())
132
131
  && evidence.keyRoleAssignmentCount === 0
133
- && Number.isSafeInteger(evidence.telegramToolCount) && evidence.telegramToolCount > 0
134
- && Number.isSafeInteger(evidence.privateToolCount) && evidence.privateToolCount > 0
135
- && evidence.relationshipProfilesExact && evidence.handlersExact
136
- && evidence.resolvedHandlerCount === evidence.telegramToolCount + evidence.privateToolCount
137
- && evidence.excludedToolCount === CONTAINMENT_EXCLUDED_TOOLS.length
138
- && evidence.excludedSchemaIntersectionCount === 0
139
- && evidence.fabricatedHandlerInvocationCount === 0
140
- && evidence.excludedToolAttemptCount === CONTAINMENT_EXCLUDED_TOOLS.length
141
- && evidence.excludedToolRejectedCount === CONTAINMENT_EXCLUDED_TOOLS.length
142
- && evidence.excludedToolInvokedCount === 0 && evidence.excludedToolSideEffectCount === 0
143
- && evidence.globallyResolvableExcludedToolCount >= 1
132
+ && (0, sanctuary_acceptance_harness_1.exactSanctuaryContainmentProfileBoundaries)(evidence.profileBoundaries)
144
133
  && evidence.auditPathDigest === (0, node_crypto_1.createHash)("sha256").update(CONTAINMENT_AUDIT_PATH).digest("hex")
145
134
  && evidence.auditRecordCount >= 2 && evidence.auditLifecyclePairCount >= 1
146
- && evidence.containerUser === "10001:10001" && evidence.liveProcessUser === "10001:10001" && evidence.mountCount === 4 && evidence.publishedPortCount === 0
147
- && evidence.networkMode === "host" && evidence.readOnlyRoot && evidence.mountsExact && evidence.securityExact && evidence.updaterDisabled
135
+ && evidence.containerUser === "10001:10001" && evidence.liveProcessUser === "10001:10001" && evidence.mountCount === 3 && evidence.publishedPortCount === 0
136
+ && evidence.networkMode === "host" && evidence.readOnlyRoot === false && evidence.mountsExact && evidence.securityExact && evidence.updaterDisabled
148
137
  && !evidence.writableKeyExposure && evidence.rawWriteMaterialFieldCount === 0 && evidence.typedWriteExecutorCount === 1
149
138
  && evidence.writeApprovalPolicyExact && !evidence.sensitiveMaterialObserved
150
139
  && digestFields.every((value) => SHA256.test(value));
@@ -499,16 +488,7 @@ function deriveSanctuaryScenarioAssertions(label, before, after, _now, scenarioH
499
488
  keyCount: after.containment.keyCount, keyInventoryDigest: after.containment.keyInventoryDigest,
500
489
  readScopeDigest: after.containment.readScopeDigest, writeScopeDigest: after.containment.writeScopeDigest,
501
490
  keyRoleAssignmentCount: after.containment.keyRoleAssignmentCount,
502
- telegramToolCount: after.containment.telegramToolCount, telegramProfileDigest: after.containment.telegramProfileDigest,
503
- telegramSchemaDigest: after.containment.telegramSchemaDigest,
504
- privateToolCount: after.containment.privateToolCount, privateProfileDigest: after.containment.privateProfileDigest,
505
- privateSchemaDigest: after.containment.privateSchemaDigest, resolvedHandlerCount: after.containment.resolvedHandlerCount,
506
- relationshipProfilesExact: after.containment.relationshipProfilesExact, handlersExact: after.containment.handlersExact,
507
- excludedToolCount: after.containment.excludedToolCount, excludedSchemaIntersectionCount: after.containment.excludedSchemaIntersectionCount,
508
- fabricatedHandlerInvocationCount: after.containment.fabricatedHandlerInvocationCount,
509
- excludedToolAttemptCount: after.containment.excludedToolAttemptCount, excludedToolRejectedCount: after.containment.excludedToolRejectedCount,
510
- excludedToolInvokedCount: after.containment.excludedToolInvokedCount, excludedToolSideEffectCount: after.containment.excludedToolSideEffectCount,
511
- globallyResolvableExcludedToolCount: after.containment.globallyResolvableExcludedToolCount,
491
+ profileBoundaries: after.containment.profileBoundaries,
512
492
  auditPathDigest: after.containment.auditPathDigest, auditLedgerDigest: after.containment.auditLedgerDigest,
513
493
  auditRecordCount: after.containment.auditRecordCount, auditLifecyclePairCount: after.containment.auditLifecyclePairCount,
514
494
  containerUser: after.containment.containerUser, liveProcessUser: after.containment.liveProcessUser, mountCount: after.containment.mountCount,