@ouro.bot/cli 0.1.0-alpha.741 → 0.1.0-alpha.743

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 (81) hide show
  1. package/changelog.json +42 -0
  2. package/deploy/unraid/Dockerfile +7 -2
  3. package/deploy/unraid/README.txt +717 -106
  4. package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
  5. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  6. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  7. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  8. package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
  9. package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
  10. package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
  12. package/deploy/unraid/sanctuary.ouro/agent.json +2 -2
  13. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
  14. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
  15. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +3 -10
  16. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
  17. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
  18. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
  19. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
  20. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
  21. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  22. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
  23. package/deploy/unraid/sanctuary.xml +12 -9
  24. package/dist/arc/cares.js +171 -37
  25. package/dist/arc/obligations.js +38 -0
  26. package/dist/heart/awaiting/await-alert.js +4 -2
  27. package/dist/heart/awaiting/await-expiry.js +39 -4
  28. package/dist/heart/awaiting/await-parser.js +8 -0
  29. package/dist/heart/context-loss-gauntlet.js +31 -2
  30. package/dist/heart/context-loss-sentinel.js +2 -0
  31. package/dist/heart/core.js +37 -7
  32. package/dist/heart/cross-chat-delivery.js +1 -0
  33. package/dist/heart/daemon/container-healthcheck.js +3 -1
  34. package/dist/heart/daemon/container-runtime.js +13 -0
  35. package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
  36. package/dist/heart/daemon/container-spec-auditor.js +38 -11
  37. package/dist/heart/daemon/daemon-entry.js +19 -5
  38. package/dist/heart/daemon/daemon.js +154 -22
  39. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +151 -87
  40. package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
  41. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
  42. package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
  43. package/dist/heart/daemon/sanctuary-scheduler-origin.js +11 -5
  44. package/dist/heart/daemon/supercronic-supervisor.js +16 -1
  45. package/dist/heart/external-events/router.js +1025 -43
  46. package/dist/heart/provider-models.js +10 -3
  47. package/dist/heart/session-events.js +23 -5
  48. package/dist/heart/steward-policy.js +302 -0
  49. package/dist/heart/tool-approval.js +3 -1
  50. package/dist/heart/turn-context.js +1 -0
  51. package/dist/mind/context.js +8 -3
  52. package/dist/mind/pending.js +25 -0
  53. package/dist/mind/prompt.js +49 -1
  54. package/dist/repertoire/guardrails.js +2 -1
  55. package/dist/repertoire/relationship-authorization.js +194 -0
  56. package/dist/repertoire/tools-awaiting.js +229 -26
  57. package/dist/repertoire/tools-base.js +15 -0
  58. package/dist/repertoire/tools-continuity.js +259 -4
  59. package/dist/repertoire/tools-notes.js +29 -3
  60. package/dist/repertoire/tools-session.js +219 -4
  61. package/dist/repertoire/tools-steward-policy.js +88 -0
  62. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  63. package/dist/repertoire/tools-unraid.js +113 -6
  64. package/dist/repertoire/tools.js +104 -9
  65. package/dist/repertoire/unraid-restart.js +86 -4
  66. package/dist/senses/private-runtime-worker.js +36 -3
  67. package/dist/senses/private-runtime.js +310 -63
  68. package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
  69. package/dist/senses/sanctuary-health-runner.js +51 -96
  70. package/dist/senses/sanctuary-health.js +54 -93
  71. package/dist/senses/sanctuary-runtime.js +36 -2
  72. package/dist/senses/sanctuary-sab.js +80 -0
  73. package/dist/senses/shared-turn.js +64 -9
  74. package/dist/senses/telegram-admission.js +684 -0
  75. package/dist/senses/telegram-approval-runtime.js +46 -6
  76. package/dist/senses/telegram-client.js +236 -72
  77. package/dist/senses/telegram-effect-adapter.js +787 -0
  78. package/dist/senses/telegram-entry.js +4 -1
  79. package/dist/senses/telegram.js +728 -48
  80. package/npm-shrinkwrap.json +6 -6
  81. package/package.json +2 -2
@@ -1,14 +1,216 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.continuityToolDefinitions = void 0;
37
+ const path = __importStar(require("node:path"));
38
+ const fs = __importStar(require("node:fs"));
4
39
  const identity_1 = require("../heart/identity");
40
+ const router_1 = require("../heart/external-events/router");
5
41
  const runtime_1 = require("../nerves/runtime");
6
42
  const episodes_1 = require("../arc/episodes");
7
43
  const cares_1 = require("../arc/cares");
8
44
  const presence_1 = require("../arc/presence");
9
45
  const intentions_1 = require("../arc/intentions");
46
+ const steward_policy_1 = require("../heart/steward-policy");
47
+ const await_parser_1 = require("../heart/awaiting/await-parser");
10
48
  exports.continuityToolDefinitions = [
11
49
  // ── Continuity tools ──────────────────────────────────────────────
50
+ {
51
+ tool: {
52
+ type: "function",
53
+ function: {
54
+ name: "external_event_disposition",
55
+ description: "Classify the exact external-event generation I just investigated. An ask or report is the sole owner-delivery path and sends reason once for this receipt; silent and act only record the disposition.",
56
+ parameters: {
57
+ type: "object",
58
+ properties: {
59
+ recordPath: { type: "string", description: "Exact receipt path from the external-event message" },
60
+ expectedGeneration: { type: "number", description: "Exact generation shown in the external-event turn" },
61
+ classifiedRevision: { type: "string", description: "Exact observation revision investigated in this turn" },
62
+ classification: { type: "string", enum: ["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"] },
63
+ stewardPolicyKind: { type: "string", enum: ["current", "none"], description: "Use current with the exact live policy key/version, or none only for a fresh observation with no applicable policy" },
64
+ stewardPolicyKey: { type: "string", description: "Exact current policy key used for this decision" },
65
+ stewardPolicyVersion: { type: "number", description: "Exact current policy version used for this decision" },
66
+ decision: { type: "string", enum: ["silent", "act", "ask", "report"] },
67
+ reason: { type: "string", description: "Short plain-language reason for the decision" },
68
+ nextWake: { type: "string", enum: ["on_change", "on_escalation", "on_recovery", "at"] },
69
+ wakeAt: { type: "string", description: "ISO time required when nextWake=at" },
70
+ awaitId: { type: "string", description: "Existing await receipt required when nextWake=at" },
71
+ careId: { type: "string", description: "Existing Care adopted for this incident, if any" },
72
+ actionRefs: { type: "array", items: { type: "string" } },
73
+ verificationRefs: { type: "array", items: { type: "string" } },
74
+ },
75
+ required: ["recordPath", "expectedGeneration", "classifiedRevision", "classification", "stewardPolicyKind", "decision", "reason", "nextWake"],
76
+ },
77
+ },
78
+ },
79
+ handler: (a, ctx) => {
80
+ const agentName = (0, identity_1.getAgentName)();
81
+ const agentEventRoot = path.resolve((0, router_1.getExternalEventRoot)(), agentName);
82
+ const recordPath = path.resolve(String(a.recordPath ?? ""));
83
+ const relative = path.relative(agentEventRoot, recordPath);
84
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || !recordPath.endsWith(".json")) {
85
+ throw new Error("External event receipt does not belong to the current agent");
86
+ }
87
+ const record = (0, router_1.readExternalEventRecord)(recordPath);
88
+ if (record.agent !== agentName)
89
+ throw new Error("External event receipt does not belong to the current agent");
90
+ const turnContext = ctx?.currentExternalEvent;
91
+ const turnEvent = turnContext
92
+ ? [turnContext, ...(turnContext.relatedEvents ?? [])].find((event) => path.resolve(event.recordPath) === recordPath)
93
+ : undefined;
94
+ const expectedGeneration = Number(a.expectedGeneration);
95
+ const classifiedRevision = String(a.classifiedRevision ?? "");
96
+ if (!turnEvent || turnEvent.recordPath !== recordPath || turnEvent.agent !== agentName
97
+ || turnEvent.generation !== expectedGeneration || turnEvent.observationRevision !== classifiedRevision
98
+ || record.generation !== expectedGeneration || record.observationRevision !== classifiedRevision
99
+ || record.executionState !== "running" || record.claimOwner !== turnEvent.claimOwner) {
100
+ throw new Error("External event disposition is not authorized for this exact turn lease");
101
+ }
102
+ const classification = String(a.classification);
103
+ const decision = String(a.decision);
104
+ const nextWake = String(a.nextWake);
105
+ const policyVersion = Number(a.stewardPolicyVersion);
106
+ const policyKind = String(a.stewardPolicyKind ?? "");
107
+ const reason = String(a.reason ?? "").trim();
108
+ if (!reason || (policyKind !== "none" && (!Number.isSafeInteger(policyVersion) || policyVersion < 1))
109
+ || !["current", "none"].includes(policyKind)
110
+ || !["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"].includes(classification)
111
+ || !["silent", "act", "ask", "report"].includes(decision)
112
+ || !["on_change", "on_escalation", "on_recovery", "at"].includes(nextWake)) {
113
+ throw new Error("External event disposition is invalid");
114
+ }
115
+ const wake = nextWake === "at" ? { kind: "at", at: String(a.wakeAt ?? "") } : { kind: nextWake };
116
+ const actionRefs = Array.isArray(a.actionRefs) ? a.actionRefs.map(String) : [];
117
+ const verificationRefs = Array.isArray(a.verificationRefs) ? a.verificationRefs.map(String) : [];
118
+ const agentRoot = (0, identity_1.getAgentRoot)();
119
+ const policy = (0, steward_policy_1.readStewardPolicy)(agentRoot);
120
+ let stewardPolicy;
121
+ if (policyKind === "none") {
122
+ if (record.transition !== "opened")
123
+ throw new Error("External event no-policy disposition requires a fresh observation");
124
+ stewardPolicy = { kind: "none" };
125
+ }
126
+ else {
127
+ const key = String(a.stewardPolicyKey ?? "").trim();
128
+ if (!key)
129
+ throw new Error("External event disposition is invalid");
130
+ if (policy.version !== policyVersion || (!policy.desiredStates[key] && !policy.routineActionGrants[key])) {
131
+ throw new Error("External event steward policy is not the exact current key/version");
132
+ }
133
+ stewardPolicy = { kind: "current", key, version: policyVersion };
134
+ }
135
+ if (typeof a.careId === "string" && a.careId) {
136
+ const care = (0, cares_1.readCares)(agentRoot).find((candidate) => candidate.id === a.careId);
137
+ const binding = care?.incidentBindings?.find((candidate) => candidate.source === record.source && candidate.incidentKey === record.eventId && candidate.classifiedRevision === classifiedRevision);
138
+ if (!care || !binding)
139
+ throw new Error("External event Care does not belong to this agent and incident revision");
140
+ }
141
+ if (classification === "adopted" && !(typeof a.careId === "string" && a.careId)) {
142
+ throw new Error("External event adopted disposition requires a Care");
143
+ }
144
+ if (nextWake === "at") {
145
+ const awaitId = typeof a.awaitId === "string" ? a.awaitId : "";
146
+ if (!/^[A-Za-z0-9_-]+$/u.test(awaitId))
147
+ throw new Error("External event timed disposition requires a current pending Await");
148
+ const awaitPath = path.join(agentRoot, "awaiting", `${awaitId}.md`);
149
+ let pendingAwait;
150
+ try {
151
+ const stat = fs.lstatSync(awaitPath);
152
+ if (!stat.isFile() || stat.isSymbolicLink())
153
+ throw new Error("unsafe Await");
154
+ pendingAwait = (0, await_parser_1.parseAwaitFile)(fs.readFileSync(awaitPath, "utf8"), awaitPath);
155
+ }
156
+ catch {
157
+ throw new Error("External event timed disposition requires a current pending Await");
158
+ }
159
+ const expectedOwnerId = ctx?.context?.friend.id;
160
+ if (pendingAwait.filed_from !== "external-event" || pendingAwait.filed_from_key !== record.recordPath
161
+ || (expectedOwnerId && pendingAwait.filed_for_friend_id !== expectedOwnerId)) {
162
+ throw new Error("External event timed disposition Await is not owned by this exact external event");
163
+ }
164
+ if (pendingAwait.status !== "pending" || pendingAwait.wake_at !== String(a.wakeAt)) {
165
+ throw new Error("External event timed disposition Await does not match the exact wake time");
166
+ }
167
+ }
168
+ const authority = ctx?.externalEventAuthority?.authorizeDisposition({
169
+ event: turnEvent,
170
+ classification,
171
+ decision,
172
+ stewardPolicy,
173
+ nextWake,
174
+ wakeAt: nextWake === "at" ? String(a.wakeAt) : null,
175
+ awaitId: typeof a.awaitId === "string" && a.awaitId ? a.awaitId : null,
176
+ careId: typeof a.careId === "string" && a.careId ? a.careId : null,
177
+ actionRefs,
178
+ verificationRefs,
179
+ });
180
+ if (!authority?.allowed)
181
+ throw new Error(`External event disposition authority denied: ${authority?.reason ?? "authority unavailable"}`);
182
+ const finish = async () => {
183
+ if (decision === "ask" || decision === "report") {
184
+ if (Buffer.byteLength(reason, "utf8") > 1_200)
185
+ throw new Error("External event owner message must be phone-sized");
186
+ if (!ctx?.externalEventEffects)
187
+ throw new Error("External event owner delivery is unavailable");
188
+ await ctx.externalEventEffects.deliverOwnerDecision({ source: record.source, eventId: record.eventId, generation: record.generation, text: reason });
189
+ }
190
+ const handled = (0, router_1.commitExternalEventDisposition)(recordPath, {
191
+ owner: turnEvent.claimOwner,
192
+ expectedVersion: record.version,
193
+ expectedGeneration: record.generation,
194
+ disposition: {
195
+ classifiedRevision,
196
+ classification: classification,
197
+ stewardPolicy,
198
+ decision: decision,
199
+ reason,
200
+ nextWake: wake,
201
+ careId: typeof a.careId === "string" && a.careId ? a.careId : null,
202
+ awaitId: typeof a.awaitId === "string" && a.awaitId ? a.awaitId : null,
203
+ actionRefs,
204
+ verificationRefs,
205
+ },
206
+ });
207
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.external_event_disposition", message: "external event disposition recorded", meta: { agentName, eventId: record.eventId, generation: record.generation, classification, decision } });
208
+ return JSON.stringify(handled, null, 2);
209
+ };
210
+ return finish();
211
+ },
212
+ riskProfile: { mutates: "durable_state_write", risk: "high", reason: "records the agent's classification on an existing external-event receipt" },
213
+ },
12
214
  {
13
215
  tool: {
14
216
  type: "function",
@@ -115,17 +317,24 @@ exports.continuityToolDefinitions = [
115
317
  type: "function",
116
318
  function: {
117
319
  name: "care_manage",
118
- description: "Create, update, or resolve a care. Cares are things I watch over — people, projects, missions, system health.",
320
+ description: "Create, update, or resolve a care, or bind and resolve one machine incident within it. Cares are things I watch over — people, projects, missions, system health.",
119
321
  parameters: {
120
322
  type: "object",
121
323
  properties: {
122
- action: { type: "string", enum: ["create", "update", "resolve"], description: "What to do" },
324
+ action: { type: "string", enum: ["create", "update", "resolve", "bind_incident", "resolve_incident", "upsert_incident"], description: "What to do" },
123
325
  id: { type: "string", description: "Care ID (required for update/resolve)" },
124
326
  label: { type: "string", description: "Short label for the care" },
125
327
  why: { type: "string", description: "Why this matters" },
126
328
  salience: { type: "string", description: "low, medium, high, or critical" },
127
329
  kind: { type: "string", description: "person, agent, project, mission, or system" },
128
330
  stewardship: { type: "string", description: "mine, shared, or delegated" },
331
+ source: { type: "string", description: "Machine evidence source for an incident binding" },
332
+ incidentKey: { type: "string", description: "Stable incident key within the source" },
333
+ classifiedRevision: { type: "string", description: "Observation revision classified by the agent" },
334
+ correlationKey: { type: "string", description: "Optional key relating several incidents to one concern" },
335
+ expectedUpdatedAt: { type: "string", description: "Optional Care updatedAt value for CAS fencing" },
336
+ currentRisk: { type: "string", description: "Current material risk for this Care, or an empty string to clear it" },
337
+ nextCheckAt: { type: "string", description: "Exact ISO time of the next owned check" },
129
338
  },
130
339
  required: ["action"],
131
340
  },
@@ -146,8 +355,8 @@ exports.continuityToolDefinitions = [
146
355
  relatedAgentIds: [],
147
356
  relatedObligationIds: [],
148
357
  relatedEpisodeIds: [],
149
- currentRisk: null,
150
- nextCheckAt: null,
358
+ currentRisk: a.currentRisk ? String(a.currentRisk) : null,
359
+ nextCheckAt: a.nextCheckAt ? String(a.nextCheckAt) : null,
151
360
  });
152
361
  }
153
362
  else if (a.action === "update") {
@@ -158,11 +367,57 @@ exports.continuityToolDefinitions = [
158
367
  updates.why = a.why;
159
368
  if (a.salience)
160
369
  updates.salience = a.salience;
370
+ if (a.currentRisk !== undefined)
371
+ updates.currentRisk = a.currentRisk || null;
372
+ if (a.nextCheckAt !== undefined)
373
+ updates.nextCheckAt = a.nextCheckAt || null;
161
374
  result = (0, cares_1.updateCare)(agentRoot, a.id, updates);
162
375
  }
163
376
  else if (a.action === "resolve") {
164
377
  result = (0, cares_1.resolveCare)(agentRoot, a.id);
165
378
  }
379
+ else if (a.action === "bind_incident") {
380
+ if (!a.expectedUpdatedAt)
381
+ throw new Error("Care incident binding requires expectedUpdatedAt");
382
+ result = (0, cares_1.bindCareIncident)(agentRoot, a.id, {
383
+ source: a.source,
384
+ incidentKey: a.incidentKey,
385
+ classifiedRevision: a.classifiedRevision,
386
+ ...(a.correlationKey ? { correlationKey: a.correlationKey } : {}),
387
+ }, { expectedUpdatedAt: a.expectedUpdatedAt });
388
+ }
389
+ else if (a.action === "resolve_incident") {
390
+ if (!a.expectedUpdatedAt)
391
+ throw new Error("Care incident resolution requires expectedUpdatedAt");
392
+ result = (0, cares_1.resolveCareIncident)(agentRoot, a.id, {
393
+ source: a.source,
394
+ incidentKey: a.incidentKey,
395
+ expectedUpdatedAt: a.expectedUpdatedAt,
396
+ });
397
+ }
398
+ else if (a.action === "upsert_incident") {
399
+ result = (0, cares_1.upsertCareForIncident)(agentRoot, {
400
+ label: a.label ?? "untitled",
401
+ why: a.why ?? "",
402
+ kind: a.kind ?? "system",
403
+ status: "active",
404
+ salience: a.salience ?? "medium",
405
+ steward: a.stewardship ?? "mine",
406
+ relatedFriendIds: [],
407
+ relatedAgentIds: [],
408
+ relatedObligationIds: [],
409
+ relatedEpisodeIds: [],
410
+ currentRisk: a.currentRisk ? String(a.currentRisk) : null,
411
+ nextCheckAt: a.nextCheckAt ? String(a.nextCheckAt) : null,
412
+ ...(a.expectedUpdatedAt ? { expectedUpdatedAt: String(a.expectedUpdatedAt) } : {}),
413
+ incident: {
414
+ source: a.source,
415
+ incidentKey: a.incidentKey,
416
+ classifiedRevision: a.classifiedRevision,
417
+ ...(a.correlationKey ? { correlationKey: a.correlationKey } : {}),
418
+ },
419
+ });
420
+ }
166
421
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.care_manage", message: `care ${a.action}`, meta: { action: a.action, id: a.id } });
167
422
  return JSON.stringify(result, null, 2);
168
423
  },
@@ -44,6 +44,7 @@ const friends_1 = require("@ouro.bot/friends");
44
44
  const diary_1 = require("../mind/diary");
45
45
  const record_paths_1 = require("../mind/record-paths");
46
46
  const provenance_trust_1 = require("../mind/provenance-trust");
47
+ const relationship_authorization_1 = require("./relationship-authorization");
47
48
  const CLAUDE_READ_ONLY_TOOLS = "Read,Grep,Glob,LS";
48
49
  function bundleRelativeLocator(filePath, ctx) {
49
50
  const agentRoot = ctx?.agentRoot ?? (0, identity_1.getAgentRoot)();
@@ -386,12 +387,13 @@ exports.notesToolDefinitions = [
386
387
  type: "function",
387
388
  function: {
388
389
  name: "save_friend_note",
389
- description: "save something i learned about my friend. use type 'name' to update their display name, 'tool_preference' for how they like a specific tool to behave (key = tool category like 'ado', 'graph'), or 'note' for general knowledge (key = topic). when updating an existing value, set override to true if i'm replacing/correcting it. omit override (or set false) if i'm unsure and want to check what's already saved.",
390
+ description: "save something i learned about my friend. use type 'name' to update their display name, 'tool_preference' for presentation preferences, or 'note' for general context. For Sanctuary relationships, preference source is stated (the person said or confirmed it), observed (a revisable pattern i inferred), or default (a fallback until better evidence), and only communication or timing are accepted. Other agents retain their existing integration-preference behavior and do not persist this provenance field. desired state and action authority belong in steward policy. notes and preferences improve orientation but never grant authority. set override=true when replacing an existing value.",
390
391
  parameters: {
391
392
  type: "object",
392
393
  properties: {
393
394
  type: { type: "string", enum: ["name", "tool_preference", "note"], description: "what kind of information to save" },
394
- key: { type: "string", description: "category key (required for tool_preference and note, e.g. 'ado', 'role')" },
395
+ key: { type: "string", description: "category key (required for tool_preference and note; Sanctuary relationship preferences use communication or timing)" },
396
+ source: { type: "string", enum: ["stated", "observed", "default"], description: "preference provenance; required for Sanctuary relationship preferences" },
395
397
  content: { type: "string", description: "the value to save" },
396
398
  override: { type: "string", enum: ["true", "false"], description: "set to 'true' to overwrite an existing value" },
397
399
  },
@@ -415,6 +417,18 @@ exports.notesToolDefinitions = [
415
417
  const friendId = ctx.context.friend?.id;
416
418
  if (!friendId)
417
419
  return "i can't save notes -- no friend identity available";
420
+ const actor = ctx.relationshipAuthorization?.actor;
421
+ const isSanctuaryRelationship = ctx.context.friend?.capabilityProfileId?.startsWith("sanctuary-") === true;
422
+ const preferenceSource = a.source;
423
+ if (isSanctuaryRelationship && a.type === "tool_preference" && !["stated", "observed", "default"].includes(String(preferenceSource))) {
424
+ return "i need exact preference provenance: source must be stated, observed, or default";
425
+ }
426
+ if (isSanctuaryRelationship && a.type === "tool_preference" && !["communication", "timing"].includes(a.key ?? "")) {
427
+ return "Sanctuary relationship preferences are limited to communication or timing; desired state and action authority belong in steward policy";
428
+ }
429
+ if (actor?.trustLevel === "friend" && (actor.friendId !== friendId || a.type !== "tool_preference" || !["communication", "timing"].includes(a.key ?? ""))) {
430
+ return "household members may only save their own communication or timing preferences; preferences never grant authority";
431
+ }
418
432
  // Validate parameters (input-shape guards stay harness-side so we keep the
419
433
  // first-person tool wording; applyFriendNote does the record mutation).
420
434
  if (!a.content)
@@ -426,9 +440,21 @@ exports.notesToolDefinitions = [
426
440
  return "i need a key for tool_preference or note type";
427
441
  // Record-domain logic lives in @ouro.bot/friends; this wrapper maps the
428
442
  // structured FriendOpResult back to the tool's first-person strings.
443
+ if (isSanctuaryRelationship && a.type === "tool_preference" && preferenceSource) {
444
+ const current = await ctx.friendStore.get(friendId);
445
+ if (!current)
446
+ return "i can't find the friend record on disk";
447
+ if (current.relationshipPolicy?.preferences[a.key] && a.override !== "true")
448
+ return `relationship preference '${a.key}' already exists; call again with override: true to replace it`;
449
+ const sourceKind = preferenceSource === "observed" ? "observed pattern" : preferenceSource === "default" ? "default fallback" : "explicit turn";
450
+ const turnSource = `${ctx.currentSession?.channel ?? "relationship"} ${sourceKind} ${actor?.sessionEventId ?? "unknown-event"}`;
451
+ await ctx.friendStore.put(friendId, (0, relationship_authorization_1.withRelationshipPreference)(current, { category: a.key, value: a.content, provenance: preferenceSource, source: turnSource }));
452
+ return `saved preference: source=${turnSource}; provenance=${preferenceSource}; category=${a.key}; value=${a.content}; this changes presentation only and grants no authority`;
453
+ }
454
+ const storedKey = a.key;
429
455
  const result = await (0, friends_1.applyFriendNote)(ctx.friendStore, friendId, {
430
456
  type: a.type,
431
- ...(a.key !== undefined ? { key: a.key } : {}),
457
+ ...(storedKey !== undefined ? { key: storedKey } : {}),
432
458
  content: a.content,
433
459
  override: a.override === "true",
434
460
  });