@ouro.bot/cli 0.1.0-alpha.733 → 0.1.0-alpha.735
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +34 -0
- package/deploy/unraid/Dockerfile +27 -0
- package/deploy/unraid/README.txt +1605 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
- package/deploy/unraid/sanctuary.xml +25 -0
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +168 -9
- package/dist/heart/auth/auth-flow.js +17 -1
- package/dist/heart/autonomy-budget.js +44 -1
- package/dist/heart/awaiting/await-scheduler.js +6 -2
- package/dist/heart/config.js +8 -0
- package/dist/heart/core.js +117 -132
- package/dist/heart/daemon/cli-exec.js +73 -1
- package/dist/heart/daemon/cli-help.js +5 -0
- package/dist/heart/daemon/cli-parse.js +6 -4
- package/dist/heart/daemon/container-credential-bootstrap.js +208 -0
- package/dist/heart/daemon/container-healthcheck.js +94 -0
- package/dist/heart/daemon/container-runtime.js +84 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
- package/dist/heart/daemon/container-spec-auditor.js +156 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
- package/dist/heart/daemon/daemon-entry.js +116 -28
- package/dist/heart/daemon/daemon-health.js +10 -0
- package/dist/heart/daemon/daemon.js +92 -5
- package/dist/heart/daemon/runtime-logging.js +6 -0
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
- package/dist/heart/daemon/sense-manager.js +25 -6
- package/dist/heart/daemon/supercronic-supervisor.js +267 -0
- package/dist/heart/habits/habit-scheduler.js +7 -3
- package/dist/heart/identity.js +13 -3
- package/dist/heart/provider-credentials.js +9 -1
- package/dist/heart/provider-models.js +8 -0
- package/dist/heart/provider-ping.js +5 -2
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/runtime-credentials.js +217 -8
- package/dist/heart/sense-truth.js +2 -0
- package/dist/heart/tool-approval.js +1 -0
- package/dist/heart/turn-context.js +8 -0
- package/dist/mind/prompt.js +9 -0
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/index.js +75 -9
- package/dist/nerves/runtime.js +12 -0
- package/dist/repertoire/tool-arguments.js +39 -0
- package/dist/repertoire/tools-unraid.js +353 -0
- package/dist/repertoire/tools.js +19 -1
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +130 -0
- package/dist/senses/private-runtime-worker.js +3 -2
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
- package/dist/senses/sanctuary-health-runner.js +146 -0
- package/dist/senses/sanctuary-health.js +447 -0
- package/dist/senses/sanctuary-interactive-control.js +306 -0
- package/dist/senses/sanctuary-runtime.js +206 -0
- package/dist/senses/shared-turn.js +11 -3
- package/dist/senses/telegram-approval-runtime.js +526 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-client.js +1471 -0
- package/dist/senses/telegram-entry.js +80 -0
- package/dist/senses/telegram.js +1232 -0
- package/npm-shrinkwrap.json +6558 -0
- package/package.json +4 -2
|
@@ -39,7 +39,9 @@ exports.readRuntimeCredentialConfig = readRuntimeCredentialConfig;
|
|
|
39
39
|
exports.readMachineRuntimeCredentialConfig = readMachineRuntimeCredentialConfig;
|
|
40
40
|
exports.cacheRuntimeCredentialConfig = cacheRuntimeCredentialConfig;
|
|
41
41
|
exports.cacheMachineRuntimeCredentialConfig = cacheMachineRuntimeCredentialConfig;
|
|
42
|
+
exports.isRuntimeCredentialBootstrapMessage = isRuntimeCredentialBootstrapMessage;
|
|
42
43
|
exports.applyRuntimeCredentialBootstrapMessage = applyRuntimeCredentialBootstrapMessage;
|
|
44
|
+
exports.persistRuntimeCredentialBootstrapMessage = persistRuntimeCredentialBootstrapMessage;
|
|
43
45
|
exports.waitForRuntimeCredentialBootstrap = waitForRuntimeCredentialBootstrap;
|
|
44
46
|
exports.refreshRuntimeCredentialConfig = refreshRuntimeCredentialConfig;
|
|
45
47
|
exports.refreshMachineRuntimeCredentialConfig = refreshMachineRuntimeCredentialConfig;
|
|
@@ -68,25 +70,54 @@ function isCredentialRecord(value) {
|
|
|
68
70
|
return false;
|
|
69
71
|
return Object.values(value).every(isCredentialValue);
|
|
70
72
|
}
|
|
73
|
+
function isPresentCredentialValue(value) {
|
|
74
|
+
if (typeof value === "string")
|
|
75
|
+
return value.trim().length > 0;
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) && value !== 0;
|
|
77
|
+
}
|
|
78
|
+
function hasExactKeys(value, keys) {
|
|
79
|
+
return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
|
|
80
|
+
}
|
|
81
|
+
function isCanonicalTimestamp(value) {
|
|
82
|
+
if (typeof value !== "string")
|
|
83
|
+
return false;
|
|
84
|
+
try {
|
|
85
|
+
return new Date(value).toISOString() === value;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
function isProviderCredentialRecord(value) {
|
|
72
92
|
if (!isRecord(value))
|
|
73
93
|
return false;
|
|
74
|
-
if (
|
|
94
|
+
if (!hasExactKeys(value, ["provider", "revision", "updatedAt", "credentials", "config", "provenance"]))
|
|
95
|
+
return false;
|
|
96
|
+
if (typeof value.provider !== "string" || !Object.prototype.hasOwnProperty.call(identity_1.PROVIDER_CREDENTIALS, value.provider))
|
|
97
|
+
return false;
|
|
98
|
+
if (typeof value.revision !== "string" || !/^vault_[0-9a-f]{16}$/u.test(value.revision))
|
|
75
99
|
return false;
|
|
76
|
-
if (
|
|
100
|
+
if (!isCanonicalTimestamp(value.updatedAt))
|
|
77
101
|
return false;
|
|
78
|
-
|
|
102
|
+
const credentials = value.credentials;
|
|
103
|
+
const config = value.config;
|
|
104
|
+
if (!isCredentialRecord(credentials))
|
|
79
105
|
return false;
|
|
80
|
-
if (!isCredentialRecord(
|
|
106
|
+
if (!isCredentialRecord(config))
|
|
81
107
|
return false;
|
|
82
|
-
|
|
108
|
+
const provider = value.provider;
|
|
109
|
+
const canonicalCredentials = (0, provider_credentials_1.splitProviderCredentialFields)(provider, credentials).credentials;
|
|
110
|
+
const canonicalConfig = (0, provider_credentials_1.splitProviderCredentialFields)(provider, config).config;
|
|
111
|
+
if (!identity_1.PROVIDER_CREDENTIALS[provider].required.every((field) => (isPresentCredentialValue(canonicalCredentials[field]) || isPresentCredentialValue(canonicalConfig[field]))))
|
|
83
112
|
return false;
|
|
84
113
|
const provenance = value.provenance;
|
|
85
114
|
if (!isRecord(provenance))
|
|
86
115
|
return false;
|
|
116
|
+
if (!hasExactKeys(provenance, ["source", "updatedAt"]))
|
|
117
|
+
return false;
|
|
87
118
|
if (provenance.source !== "auth-flow" && provenance.source !== "manual")
|
|
88
119
|
return false;
|
|
89
|
-
if (
|
|
120
|
+
if (!isCanonicalTimestamp(provenance.updatedAt) || provenance.updatedAt !== value.updatedAt)
|
|
90
121
|
return false;
|
|
91
122
|
return true;
|
|
92
123
|
}
|
|
@@ -98,6 +129,55 @@ function stableJson(value) {
|
|
|
98
129
|
}
|
|
99
130
|
return JSON.stringify(value);
|
|
100
131
|
}
|
|
132
|
+
function reconcileBootstrapFields(base, bootstrap, prefix) {
|
|
133
|
+
const conflicts = [];
|
|
134
|
+
const merge = (current, incoming, pathPrefix) => {
|
|
135
|
+
const next = { ...current };
|
|
136
|
+
for (const [key, incomingValue] of Object.entries(incoming)) {
|
|
137
|
+
const fieldPath = `${pathPrefix}.${key}`;
|
|
138
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) {
|
|
139
|
+
next[key] = incomingValue;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const currentValue = current[key];
|
|
143
|
+
if (isRecord(currentValue) && isRecord(incomingValue)) {
|
|
144
|
+
next[key] = merge(currentValue, incomingValue, fieldPath);
|
|
145
|
+
}
|
|
146
|
+
else if (stableJson(currentValue) !== stableJson(incomingValue)) {
|
|
147
|
+
conflicts.push(fieldPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return next;
|
|
151
|
+
};
|
|
152
|
+
const merged = merge(base, bootstrap, prefix);
|
|
153
|
+
if (conflicts.length > 0) {
|
|
154
|
+
throw new Error(`container credential bootstrap conflicts with existing fields: ${conflicts.sort().join(", ")}`);
|
|
155
|
+
}
|
|
156
|
+
return merged;
|
|
157
|
+
}
|
|
158
|
+
function assertBootstrapReadback(actual, expected, prefix) {
|
|
159
|
+
const failures = [];
|
|
160
|
+
const inspect = (current, intended, pathPrefix) => {
|
|
161
|
+
for (const [key, intendedValue] of Object.entries(intended)) {
|
|
162
|
+
const fieldPath = `${pathPrefix}.${key}`;
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) {
|
|
164
|
+
failures.push(fieldPath);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const currentValue = current[key];
|
|
168
|
+
if (isRecord(currentValue) && isRecord(intendedValue)) {
|
|
169
|
+
inspect(currentValue, intendedValue, fieldPath);
|
|
170
|
+
}
|
|
171
|
+
else if (stableJson(currentValue) !== stableJson(intendedValue)) {
|
|
172
|
+
failures.push(fieldPath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
inspect(actual, expected, prefix);
|
|
177
|
+
if (failures.length > 0) {
|
|
178
|
+
throw new Error(`container credential bootstrap readback did not contain intended fields: ${failures.sort().join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
101
181
|
function runtimeConfigVaultPath(agentName, itemName = exports.RUNTIME_CONFIG_ITEM_NAME) {
|
|
102
182
|
return `vault:${agentName}:${itemName}`;
|
|
103
183
|
}
|
|
@@ -198,20 +278,41 @@ function isRuntimeCredentialBootstrapMessage(value) {
|
|
|
198
278
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
199
279
|
return false;
|
|
200
280
|
const record = value;
|
|
281
|
+
if (!Object.keys(record).every((key) => [
|
|
282
|
+
"type",
|
|
283
|
+
"agentName",
|
|
284
|
+
"runtimeConfig",
|
|
285
|
+
"machineRuntimeConfig",
|
|
286
|
+
"machineId",
|
|
287
|
+
"providerCredentialRecords",
|
|
288
|
+
].includes(key)))
|
|
289
|
+
return false;
|
|
201
290
|
if (record.type !== "ouro.runtimeCredentialBootstrap")
|
|
202
291
|
return false;
|
|
203
292
|
if (typeof record.agentName !== "string" || record.agentName.trim().length === 0)
|
|
204
293
|
return false;
|
|
205
|
-
if (record.runtimeConfig !== undefined
|
|
294
|
+
if (record.runtimeConfig !== undefined
|
|
295
|
+
&& (!isRecord(record.runtimeConfig) || Object.keys(record.runtimeConfig).length === 0))
|
|
206
296
|
return false;
|
|
207
|
-
if (record.machineRuntimeConfig !== undefined
|
|
297
|
+
if (record.machineRuntimeConfig !== undefined
|
|
298
|
+
&& (!isRecord(record.machineRuntimeConfig) || Object.keys(record.machineRuntimeConfig).length === 0))
|
|
208
299
|
return false;
|
|
209
300
|
if (record.machineId !== undefined && (typeof record.machineId !== "string" || record.machineId.trim().length === 0))
|
|
210
301
|
return false;
|
|
211
302
|
if (record.providerCredentialRecords !== undefined
|
|
212
303
|
&& (!Array.isArray(record.providerCredentialRecords)
|
|
304
|
+
|| record.providerCredentialRecords.length === 0
|
|
213
305
|
|| !record.providerCredentialRecords.every(isProviderCredentialRecord)))
|
|
214
306
|
return false;
|
|
307
|
+
if (Array.isArray(record.providerCredentialRecords)) {
|
|
308
|
+
const providers = record.providerCredentialRecords.map((providerRecord) => providerRecord.provider);
|
|
309
|
+
if (new Set(providers).size !== providers.length)
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (record.runtimeConfig === undefined
|
|
313
|
+
&& record.machineRuntimeConfig === undefined
|
|
314
|
+
&& (!Array.isArray(record.providerCredentialRecords) || record.providerCredentialRecords.length === 0))
|
|
315
|
+
return false;
|
|
215
316
|
return true;
|
|
216
317
|
}
|
|
217
318
|
function applyRuntimeCredentialBootstrapMessage(message) {
|
|
@@ -241,6 +342,114 @@ function applyRuntimeCredentialBootstrapMessage(message) {
|
|
|
241
342
|
});
|
|
242
343
|
return true;
|
|
243
344
|
}
|
|
345
|
+
async function persistRuntimeCredentialBootstrapMessage(message, options) {
|
|
346
|
+
if (!isRuntimeCredentialBootstrapMessage(message))
|
|
347
|
+
return false;
|
|
348
|
+
const agentName = message.agentName.trim();
|
|
349
|
+
const now = options.now ?? new Date();
|
|
350
|
+
if (message.machineId !== undefined && message.machineId.trim() !== options.machineId.trim()) {
|
|
351
|
+
throw new Error("container credential bootstrap machineId does not match this machine");
|
|
352
|
+
}
|
|
353
|
+
let runtimePlan = null;
|
|
354
|
+
let machinePlan = null;
|
|
355
|
+
let expectedRuntime = null;
|
|
356
|
+
let expectedMachine = null;
|
|
357
|
+
const providerPlans = [];
|
|
358
|
+
const expectedProviders = new Map();
|
|
359
|
+
if (message.runtimeConfig) {
|
|
360
|
+
const current = await refreshRuntimeCredentialConfig(agentName);
|
|
361
|
+
if (!current.ok && current.reason !== "missing") {
|
|
362
|
+
throw new Error(`cannot reconcile runtimeConfig at ${current.itemPath}: ${current.error}`);
|
|
363
|
+
}
|
|
364
|
+
const base = current.ok ? current.config : {};
|
|
365
|
+
const merged = reconcileBootstrapFields(base, message.runtimeConfig, "runtimeConfig");
|
|
366
|
+
expectedRuntime = merged;
|
|
367
|
+
if (stableJson(merged) !== stableJson(base))
|
|
368
|
+
runtimePlan = merged;
|
|
369
|
+
}
|
|
370
|
+
if (message.machineRuntimeConfig) {
|
|
371
|
+
const machineId = message.machineId ?? options.machineId;
|
|
372
|
+
const current = await refreshMachineRuntimeCredentialConfig(agentName, machineId);
|
|
373
|
+
if (!current.ok && current.reason !== "missing") {
|
|
374
|
+
throw new Error(`cannot reconcile machineRuntimeConfig at ${current.itemPath}: ${current.error}`);
|
|
375
|
+
}
|
|
376
|
+
const base = current.ok ? current.config : {};
|
|
377
|
+
const merged = reconcileBootstrapFields(base, message.machineRuntimeConfig, "machineRuntimeConfig");
|
|
378
|
+
expectedMachine = merged;
|
|
379
|
+
if (stableJson(merged) !== stableJson(base))
|
|
380
|
+
machinePlan = merged;
|
|
381
|
+
}
|
|
382
|
+
for (const record of message.providerCredentialRecords ?? []) {
|
|
383
|
+
const current = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, { providers: [record.provider], skipCache: true });
|
|
384
|
+
if (!current.ok && current.reason !== "missing") {
|
|
385
|
+
throw new Error(`cannot reconcile providerCredentialRecords.${record.provider} at ${current.poolPath}: ${current.error}`);
|
|
386
|
+
}
|
|
387
|
+
const existing = current.ok ? current.pool.providers[record.provider] : undefined;
|
|
388
|
+
const credentials = reconcileBootstrapFields(existing?.credentials ?? {}, record.credentials, `providerCredentialRecords.${record.provider}.credentials`);
|
|
389
|
+
const config = reconcileBootstrapFields(existing?.config ?? {}, record.config, `providerCredentialRecords.${record.provider}.config`);
|
|
390
|
+
expectedProviders.set(record.provider, { credentials, config });
|
|
391
|
+
if (!existing || stableJson(credentials) !== stableJson(existing.credentials) || stableJson(config) !== stableJson(existing.config)) {
|
|
392
|
+
providerPlans.push({
|
|
393
|
+
provider: record.provider,
|
|
394
|
+
credentials,
|
|
395
|
+
config,
|
|
396
|
+
provenanceSource: existing?.provenance.source ?? record.provenance.source,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (runtimePlan)
|
|
401
|
+
await upsertRuntimeCredentialConfig(agentName, runtimePlan, now);
|
|
402
|
+
if (machinePlan) {
|
|
403
|
+
await upsertMachineRuntimeCredentialConfig(agentName, message.machineId ?? options.machineId, machinePlan, now);
|
|
404
|
+
}
|
|
405
|
+
for (const plan of providerPlans) {
|
|
406
|
+
await (0, provider_credentials_1.upsertProviderCredential)({
|
|
407
|
+
agentName,
|
|
408
|
+
provider: plan.provider,
|
|
409
|
+
credentials: plan.credentials,
|
|
410
|
+
config: plan.config,
|
|
411
|
+
provenance: { source: plan.provenanceSource },
|
|
412
|
+
now,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (message.runtimeConfig) {
|
|
416
|
+
const refreshed = await refreshRuntimeCredentialConfig(agentName);
|
|
417
|
+
if (!refreshed.ok)
|
|
418
|
+
throw new Error(`cannot read back runtimeConfig at ${refreshed.itemPath}: ${refreshed.error}`);
|
|
419
|
+
assertBootstrapReadback(refreshed.config, expectedRuntime, "runtimeConfig");
|
|
420
|
+
}
|
|
421
|
+
if (message.machineRuntimeConfig) {
|
|
422
|
+
const refreshed = await refreshMachineRuntimeCredentialConfig(agentName, message.machineId ?? options.machineId);
|
|
423
|
+
if (!refreshed.ok)
|
|
424
|
+
throw new Error(`cannot read back machineRuntimeConfig at ${refreshed.itemPath}: ${refreshed.error}`);
|
|
425
|
+
assertBootstrapReadback(refreshed.config, expectedMachine, "machineRuntimeConfig");
|
|
426
|
+
}
|
|
427
|
+
if ((message.providerCredentialRecords?.length ?? 0) > 0) {
|
|
428
|
+
const refreshed = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName);
|
|
429
|
+
if (!refreshed.ok)
|
|
430
|
+
throw new Error(`cannot read back providerCredentialRecords at ${refreshed.poolPath}: ${refreshed.error}`);
|
|
431
|
+
for (const [provider, expected] of expectedProviders) {
|
|
432
|
+
const record = refreshed.pool.providers[provider];
|
|
433
|
+
if (!record) {
|
|
434
|
+
throw new Error(`container credential bootstrap readback did not contain intended fields: providerCredentialRecords.${provider}`);
|
|
435
|
+
}
|
|
436
|
+
assertBootstrapReadback(record.credentials, expected.credentials, `providerCredentialRecords.${provider}.credentials`);
|
|
437
|
+
assertBootstrapReadback(record.config, expected.config, `providerCredentialRecords.${provider}.config`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
(0, runtime_1.emitNervesEvent)({
|
|
441
|
+
component: "config/identity",
|
|
442
|
+
event: "config.runtime_credentials_bootstrap_persisted",
|
|
443
|
+
message: "persisted container credential bootstrap in the agent vault",
|
|
444
|
+
meta: {
|
|
445
|
+
agentName,
|
|
446
|
+
runtimeConfig: !!message.runtimeConfig,
|
|
447
|
+
machineRuntimeConfig: !!message.machineRuntimeConfig,
|
|
448
|
+
providerCredentialRecords: message.providerCredentialRecords?.length ?? 0,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
244
453
|
function waitForRuntimeCredentialBootstrap(agentName, options = {}) {
|
|
245
454
|
const timeoutMs = options.timeoutMs ?? 1_500;
|
|
246
455
|
return new Promise((resolve) => {
|
|
@@ -10,6 +10,7 @@ const SENSES = [
|
|
|
10
10
|
{ sense: "mail", label: "Mail", daemonManaged: true },
|
|
11
11
|
{ sense: "voice", label: "Voice", daemonManaged: true },
|
|
12
12
|
{ sense: "a2a", label: "A2A", daemonManaged: true },
|
|
13
|
+
{ sense: "telegram", label: "Telegram", daemonManaged: true },
|
|
13
14
|
{ sense: "workbench", label: "Workbench", daemonManaged: false },
|
|
14
15
|
];
|
|
15
16
|
function configuredSenses(senses) {
|
|
@@ -22,6 +23,7 @@ function configuredSenses(senses) {
|
|
|
22
23
|
mail: configured.mail ?? { ...identity_1.DEFAULT_AGENT_SENSES.mail },
|
|
23
24
|
voice: configured.voice ?? { ...identity_1.DEFAULT_AGENT_SENSES.voice },
|
|
24
25
|
a2a: configured.a2a ?? { ...identity_1.DEFAULT_AGENT_SENSES.a2a },
|
|
26
|
+
telegram: configured.telegram ?? { ...identity_1.DEFAULT_AGENT_SENSES.telegram },
|
|
25
27
|
workbench: configured.workbench ?? { ...identity_1.DEFAULT_AGENT_SENSES.workbench },
|
|
26
28
|
};
|
|
27
29
|
}
|
|
@@ -297,6 +297,7 @@ async function executeApprovalDecision(options) {
|
|
|
297
297
|
catch (error) {
|
|
298
298
|
if (!(error instanceof ApprovalExecutionFailedError))
|
|
299
299
|
throw error;
|
|
300
|
+
await options.hooks?.afterHandler?.();
|
|
300
301
|
return options.approvalStore.complete({
|
|
301
302
|
approvalId: attempted.approvalId,
|
|
302
303
|
ownerId: attempted.ownerId,
|
|
@@ -164,6 +164,7 @@ function readSenseStatusLines() {
|
|
|
164
164
|
mail: configuredSenses.mail ?? { enabled: false },
|
|
165
165
|
voice: configuredSenses.voice ?? { enabled: false },
|
|
166
166
|
a2a: configuredSenses.a2a ?? { enabled: false },
|
|
167
|
+
telegram: configuredSenses.telegram ?? { enabled: false },
|
|
167
168
|
workbench: configuredSenses.workbench ?? { enabled: false },
|
|
168
169
|
};
|
|
169
170
|
const payload = (0, config_1.loadConfig)();
|
|
@@ -210,6 +211,9 @@ function readSenseStatusLines() {
|
|
|
210
211
|
? openAIRealtimeVoiceReady
|
|
211
212
|
: cascadeVoiceReady,
|
|
212
213
|
a2a: true,
|
|
214
|
+
telegram: hasTextField(runtimePayload, "telegramBotToken")
|
|
215
|
+
&& hasTextField(runtimePayload, "telegramAuthorizedUserId")
|
|
216
|
+
&& hasTextField(runtimePayload, "telegramAuthorizedChatId"),
|
|
213
217
|
workbench: false,
|
|
214
218
|
};
|
|
215
219
|
const rows = [
|
|
@@ -234,6 +238,10 @@ function readSenseStatusLines() {
|
|
|
234
238
|
label: "A2A",
|
|
235
239
|
status: senses.a2a.enabled ? "ready" : "disabled",
|
|
236
240
|
},
|
|
241
|
+
{
|
|
242
|
+
label: "Telegram",
|
|
243
|
+
status: !senses.telegram.enabled ? "disabled" : configured.telegram ? "ready" : "needs_config",
|
|
244
|
+
},
|
|
237
245
|
{
|
|
238
246
|
label: "Workbench",
|
|
239
247
|
// Workbench can be injected at runtime (the Workbench app spawns
|
package/dist/mind/prompt.js
CHANGED
|
@@ -348,6 +348,7 @@ const PROCESS_TYPE_LABELS = {
|
|
|
348
348
|
voice: "voice handler",
|
|
349
349
|
a2a: "a2a handler",
|
|
350
350
|
mcp: "mcp bridge",
|
|
351
|
+
telegram: "telegram handler",
|
|
351
352
|
};
|
|
352
353
|
function processTypeLabel(channel) {
|
|
353
354
|
return PROCESS_TYPE_LABELS[channel];
|
|
@@ -468,6 +469,7 @@ function localSenseStatusLines() {
|
|
|
468
469
|
mail: configuredSenses.mail ?? { enabled: false },
|
|
469
470
|
voice: configuredSenses.voice ?? { enabled: false },
|
|
470
471
|
a2a: configuredSenses.a2a ?? { enabled: false },
|
|
472
|
+
telegram: configuredSenses.telegram ?? { enabled: false },
|
|
471
473
|
workbench: configuredSenses.workbench ?? { enabled: false },
|
|
472
474
|
};
|
|
473
475
|
const payload = (0, config_1.loadConfig)();
|
|
@@ -493,6 +495,9 @@ function localSenseStatusLines() {
|
|
|
493
495
|
&& hasTextField(voice, "whisperCliPath")
|
|
494
496
|
&& hasTextField(voice, "whisperModelPath"),
|
|
495
497
|
a2a: true,
|
|
498
|
+
telegram: hasTextField(runtimePayload, "telegramBotToken")
|
|
499
|
+
&& hasTextField(runtimePayload, "telegramAuthorizedUserId")
|
|
500
|
+
&& hasTextField(runtimePayload, "telegramAuthorizedChatId"),
|
|
496
501
|
workbench: false,
|
|
497
502
|
};
|
|
498
503
|
const rows = [
|
|
@@ -517,6 +522,10 @@ function localSenseStatusLines() {
|
|
|
517
522
|
label: "A2A",
|
|
518
523
|
status: senses.a2a.enabled ? "ready" : "disabled",
|
|
519
524
|
},
|
|
525
|
+
{
|
|
526
|
+
label: "Telegram",
|
|
527
|
+
status: !senses.telegram.enabled ? "disabled" : configured.telegram ? "ready" : "needs_config",
|
|
528
|
+
},
|
|
520
529
|
{
|
|
521
530
|
label: "Workbench",
|
|
522
531
|
// Workbench can be injected at runtime (the Workbench app spawns
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Static source scanner for emitNervesEvent calls.
|
|
3
|
+
* Static source scanner for emitNervesEvent and emitNervesEventDurable calls.
|
|
4
4
|
*
|
|
5
5
|
* Extracts component:event keys from production source files by
|
|
6
|
-
* regex-matching
|
|
6
|
+
* regex-matching either static emitter with literal component/event fields.
|
|
7
7
|
* calls. Only accepts static string literals (single or double quotes).
|
|
8
8
|
* Template literals and variable references are rejected.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.scanSourceForNervesKeys = scanSourceForNervesKeys;
|
|
12
|
-
const EMIT_CALL_RE = /emitNervesEvent
|
|
12
|
+
const EMIT_CALL_RE = /emitNervesEvent(?:Durable)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
|
|
13
13
|
function extractStringLiteral(block, field) {
|
|
14
14
|
const re = new RegExp(`${field}\\s*:\\s*(['"])((?:(?!\\1).)+)\\1`);
|
|
15
15
|
const match = re.exec(block);
|
|
16
16
|
return match ? match[2] : null;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Scan a source file's content for
|
|
19
|
+
* Scan a source file's content for synchronous or durable Nerves calls and extract
|
|
20
20
|
* component:event keys. Only static string literals are accepted.
|
|
21
21
|
*/
|
|
22
22
|
function scanSourceForNervesKeys(source) {
|
package/dist/nerves/index.js
CHANGED
|
@@ -61,6 +61,24 @@ const LEVEL_PRIORITY = {
|
|
|
61
61
|
warn: 30,
|
|
62
62
|
error: 40,
|
|
63
63
|
};
|
|
64
|
+
function fsyncFileAndDirectory(filePath) {
|
|
65
|
+
if ((0, fs_1.existsSync)(filePath)) {
|
|
66
|
+
const fileDescriptor = (0, fs_1.openSync)(filePath, "r");
|
|
67
|
+
try {
|
|
68
|
+
(0, fs_1.fsyncSync)(fileDescriptor);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
(0, fs_1.closeSync)(fileDescriptor);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const directoryDescriptor = (0, fs_1.openSync)((0, path_1.dirname)(filePath), "r");
|
|
75
|
+
try {
|
|
76
|
+
(0, fs_1.fsyncSync)(directoryDescriptor);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
(0, fs_1.closeSync)(directoryDescriptor);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
64
82
|
const GLOBAL_SINKS_KEY = Symbol.for("ouroboros.nerves.global-sinks");
|
|
65
83
|
function resolveGlobalSinks() {
|
|
66
84
|
const scope = globalThis;
|
|
@@ -248,6 +266,7 @@ function rotateIfNeeded(filePath, optionsOrMaxSize) {
|
|
|
248
266
|
if ((0, fs_1.existsSync)(plain1)) {
|
|
249
267
|
(0, fs_1.unlinkSync)(plain1);
|
|
250
268
|
}
|
|
269
|
+
fsyncFileAndDirectory(filePath);
|
|
251
270
|
(0, fs_1.renameSync)(filePath, plain1);
|
|
252
271
|
// Step 3: gzip (or keep plain) the renamed file into the .1 generation.
|
|
253
272
|
if (compress) {
|
|
@@ -259,8 +278,10 @@ function rotateIfNeeded(filePath, optionsOrMaxSize) {
|
|
|
259
278
|
const buf = (0, fs_1.readFileSync)(plain1);
|
|
260
279
|
const compressed = zlib.gzipSync(buf);
|
|
261
280
|
(0, fs_1.writeFileSync)(gz1, compressed);
|
|
281
|
+
fsyncFileAndDirectory(gz1);
|
|
262
282
|
(0, fs_1.unlinkSync)(plain1);
|
|
263
283
|
}
|
|
284
|
+
fsyncFileAndDirectory(compress ? generationGzPath(base, ext, 1) : plain1);
|
|
264
285
|
completed = true;
|
|
265
286
|
(0, runtime_1.emitNervesEvent)({
|
|
266
287
|
component: "nerves",
|
|
@@ -298,6 +319,33 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
|
|
|
298
319
|
const queue = [];
|
|
299
320
|
let flushing = false;
|
|
300
321
|
let bytesSinceCheck = 0;
|
|
322
|
+
let enqueued = 0;
|
|
323
|
+
let completed = 0;
|
|
324
|
+
let appendFailure = null;
|
|
325
|
+
const barriers = [];
|
|
326
|
+
function settleBarriers() {
|
|
327
|
+
const ready = barriers.filter((barrier) => barrier.target <= completed);
|
|
328
|
+
if (ready.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
for (const barrier of ready)
|
|
331
|
+
barriers.splice(barriers.indexOf(barrier), 1);
|
|
332
|
+
if (appendFailure) {
|
|
333
|
+
for (const barrier of ready)
|
|
334
|
+
barrier.reject(appendFailure);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
fsyncFileAndDirectory(filePath);
|
|
339
|
+
for (const barrier of ready)
|
|
340
|
+
barrier.resolve();
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
344
|
+
appendFailure = failure;
|
|
345
|
+
for (const barrier of ready)
|
|
346
|
+
barrier.reject(failure);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
301
349
|
function flush() {
|
|
302
350
|
if (flushing || queue.length === 0)
|
|
303
351
|
return;
|
|
@@ -310,31 +358,42 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
|
|
|
310
358
|
try {
|
|
311
359
|
rotateIfNeeded(filePath, options);
|
|
312
360
|
}
|
|
313
|
-
catch {
|
|
314
|
-
|
|
361
|
+
catch (rotationError) {
|
|
362
|
+
appendFailure ??= rotationError instanceof Error ? rotationError : new Error(String(rotationError));
|
|
315
363
|
}
|
|
316
364
|
}
|
|
317
|
-
(0, fs_1.appendFile)(filePath, line, "utf8", () => {
|
|
365
|
+
(0, fs_1.appendFile)(filePath, line, "utf8", (error) => {
|
|
366
|
+
completed += 1;
|
|
367
|
+
if (error)
|
|
368
|
+
appendFailure ??= error;
|
|
318
369
|
if (checkAfterAppend) {
|
|
319
370
|
try {
|
|
371
|
+
fsyncFileAndDirectory(filePath);
|
|
320
372
|
rotateIfNeeded(filePath, options);
|
|
321
373
|
}
|
|
322
|
-
catch {
|
|
323
|
-
|
|
374
|
+
catch (rotationError) {
|
|
375
|
+
appendFailure ??= rotationError instanceof Error ? rotationError : new Error(String(rotationError));
|
|
324
376
|
}
|
|
325
377
|
}
|
|
378
|
+
settleBarriers();
|
|
326
379
|
flushing = false;
|
|
327
380
|
flush();
|
|
328
381
|
});
|
|
329
382
|
}
|
|
330
|
-
|
|
383
|
+
const sink = ((entry) => {
|
|
331
384
|
const line = verbose
|
|
332
385
|
? `${JSON.stringify(entry)}\n`
|
|
333
386
|
: `${(0, redact_1.redactString)(JSON.stringify((0, redact_1.redactLogEntry)(entry)))}\n`;
|
|
334
387
|
bytesSinceCheck += line.length;
|
|
388
|
+
enqueued += 1;
|
|
335
389
|
queue.push(line);
|
|
336
390
|
flush();
|
|
337
|
-
};
|
|
391
|
+
});
|
|
392
|
+
sink.barrier = () => new Promise((resolve, reject) => {
|
|
393
|
+
barriers.push({ target: enqueued, resolve, reject });
|
|
394
|
+
settleBarriers();
|
|
395
|
+
});
|
|
396
|
+
return sink;
|
|
338
397
|
}
|
|
339
398
|
function registerGlobalLogSink(sink) {
|
|
340
399
|
globalSinks.add(sink);
|
|
@@ -356,9 +415,10 @@ function createLogger(options = {}) {
|
|
|
356
415
|
const configuredLevel = options.level ?? "info";
|
|
357
416
|
const sinks = options.sinks ?? [createStderrSink()];
|
|
358
417
|
const sink = createFanoutSink(sinks);
|
|
418
|
+
const durableSinks = sinks.filter((candidate) => "barrier" in candidate && typeof candidate.barrier === "function");
|
|
359
419
|
const now = options.now ?? (() => new Date());
|
|
360
|
-
function emit(level, entry) {
|
|
361
|
-
if (!shouldEmit(configuredLevel, level)) {
|
|
420
|
+
function emit(level, entry, force = false) {
|
|
421
|
+
if (!force && !shouldEmit(configuredLevel, level)) {
|
|
362
422
|
return;
|
|
363
423
|
}
|
|
364
424
|
const payload = {
|
|
@@ -378,5 +438,11 @@ function createLogger(options = {}) {
|
|
|
378
438
|
info: (entry) => emit("info", entry),
|
|
379
439
|
warn: (entry) => emit("warn", entry),
|
|
380
440
|
error: (entry) => emit("error", entry),
|
|
441
|
+
durabilityBarrier: async () => {
|
|
442
|
+
if (durableSinks.length === 0)
|
|
443
|
+
throw new Error("No durable Nerves sink is configured");
|
|
444
|
+
await Promise.all(durableSinks.map((durableSink) => durableSink.barrier()));
|
|
445
|
+
},
|
|
446
|
+
emitDurable: (level, entry) => emit(level, entry, true),
|
|
381
447
|
};
|
|
382
448
|
}
|
package/dist/nerves/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.setRuntimeLogger = setRuntimeLogger;
|
|
4
4
|
exports.emitNervesEvent = emitNervesEvent;
|
|
5
|
+
exports.emitNervesEventDurable = emitNervesEventDurable;
|
|
5
6
|
const index_1 = require("./index");
|
|
6
7
|
let runtimeLogger = null;
|
|
7
8
|
function getRuntimeLogger() {
|
|
@@ -40,3 +41,14 @@ function emitNervesEvent(event) {
|
|
|
40
41
|
logger.info(payload);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
async function emitNervesEventDurable(event) {
|
|
45
|
+
const logger = getRuntimeLogger();
|
|
46
|
+
logger.emitDurable(event.level ?? "info", {
|
|
47
|
+
event: event.event,
|
|
48
|
+
trace_id: (0, index_1.ensureTraceId)(event.trace_id),
|
|
49
|
+
component: event.component,
|
|
50
|
+
message: event.message,
|
|
51
|
+
meta: event.meta ?? {},
|
|
52
|
+
});
|
|
53
|
+
await logger.durabilityBarrier();
|
|
54
|
+
}
|
|
@@ -16,6 +16,34 @@ const ajv = new ajv_1.default({
|
|
|
16
16
|
useDefaults: false,
|
|
17
17
|
});
|
|
18
18
|
const validators = new WeakMap();
|
|
19
|
+
function unsupportedSchemaFeature(value, seen = new WeakSet()) {
|
|
20
|
+
if (value === null)
|
|
21
|
+
return "null schema values are unsupported";
|
|
22
|
+
if (typeof value !== "object")
|
|
23
|
+
return undefined;
|
|
24
|
+
if (seen.has(value))
|
|
25
|
+
return "cyclic schemas are unsupported";
|
|
26
|
+
seen.add(value);
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const entry of value) {
|
|
29
|
+
const reason = unsupportedSchemaFeature(entry, seen);
|
|
30
|
+
if (reason)
|
|
31
|
+
return reason;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
36
|
+
if (key === "patternProperties")
|
|
37
|
+
return "patternProperties is unsupported";
|
|
38
|
+
if (key === "type" && (entry === "null" || (Array.isArray(entry) && entry.includes("null")))) {
|
|
39
|
+
return "null schema types are unsupported";
|
|
40
|
+
}
|
|
41
|
+
const reason = unsupportedSchemaFeature(entry, seen);
|
|
42
|
+
if (reason)
|
|
43
|
+
return reason;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
19
47
|
function canonicalize(value) {
|
|
20
48
|
if (value === null || typeof value !== "object")
|
|
21
49
|
return JSON.stringify(value);
|
|
@@ -33,6 +61,17 @@ function digestJson(value) {
|
|
|
33
61
|
return digest(canonicalize(value));
|
|
34
62
|
}
|
|
35
63
|
function validateAdvertisedToolArguments(rawArguments, schema) {
|
|
64
|
+
const unsupported = unsupportedSchemaFeature(schema);
|
|
65
|
+
if (unsupported) {
|
|
66
|
+
(0, runtime_1.emitNervesEvent)({
|
|
67
|
+
level: "error",
|
|
68
|
+
component: "repertoire",
|
|
69
|
+
event: "repertoire.tool_schema_unsupported",
|
|
70
|
+
message: "advertised tool schema used an unsupported feature",
|
|
71
|
+
meta: { reason: unsupported },
|
|
72
|
+
});
|
|
73
|
+
return { ok: false, reason: `advertised schema is unsupported: ${unsupported}` };
|
|
74
|
+
}
|
|
36
75
|
let parsed;
|
|
37
76
|
try {
|
|
38
77
|
parsed = JSON.parse(rawArguments);
|