@ouro.bot/cli 0.1.0-alpha.990 → 0.1.0-alpha.991
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 +6 -0
- package/deploy/unraid/Dockerfile +15 -2
- package/deploy/unraid/README.txt +44 -9
- package/deploy/unraid/github_known_hosts +3 -0
- package/deploy/unraid/ouro-sync-ssh +98 -0
- package/deploy/unraid/ouro-sync.sudoers +3 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +26 -2
- package/deploy/unraid/sanctuary-replay-gate.mjs +361 -8
- package/deploy/unraid/sanctuary-sync-cutover.sh +1039 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +2 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +3 -1
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/config.js +2 -1
- package/dist/heart/daemon/agent-discovery.js +5 -1
- package/dist/heart/daemon/agent-home.js +67 -31
- package/dist/heart/daemon/boot-sync-probe.js +36 -10
- package/dist/heart/daemon/cli-exec.js +79 -20
- package/dist/heart/daemon/cli-help.js +12 -0
- package/dist/heart/daemon/cli-parse.js +14 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +22 -6
- package/dist/heart/daemon/container-spec-auditor.js +18 -2
- package/dist/heart/daemon/daemon-entry.js +22 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
- package/dist/heart/daemon/sanctuary-bundle-migration.js +97 -1
- package/dist/heart/daemon/sanctuary-package-management.js +10 -0
- package/dist/heart/sync-lock.js +431 -0
- package/dist/heart/sync.js +1252 -399
- package/dist/mind/prompt.js +92 -19
- package/dist/repertoire/guardrails.js +285 -3
- package/dist/repertoire/tools-bundle.js +107 -185
- package/dist/repertoire/tools.js +5 -1
- package/dist/senses/pipeline.js +37 -3
- package/dist/senses/private-runtime.js +1 -6
- package/dist/senses/voice/twilio-phone.js +16 -16
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
exports.runMcpServeCliLifecycle = runMcpServeCliLifecycle;
|
|
43
43
|
exports.summarizeDaemonStartupFailure = summarizeDaemonStartupFailure;
|
|
44
44
|
exports.writeSyncProbeSummary = writeSyncProbeSummary;
|
|
45
|
+
exports.writeSyncProbeStatuses = writeSyncProbeStatuses;
|
|
45
46
|
exports.summarizeSyncProbeFindings = summarizeSyncProbeFindings;
|
|
46
47
|
exports.mergeStartupStability = mergeStartupStability;
|
|
47
48
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
@@ -54,6 +55,7 @@ exports.resolveMailImportFilePath = resolveMailImportFilePath;
|
|
|
54
55
|
exports.runOuroCli = runOuroCli;
|
|
55
56
|
const child_process_1 = require("child_process");
|
|
56
57
|
const bluebubbles_host_account_1 = require("../bluebubbles-host-account");
|
|
58
|
+
const sanctuary_bundle_migration_1 = require("./sanctuary-bundle-migration");
|
|
57
59
|
const crypto_1 = require("crypto");
|
|
58
60
|
const fs = __importStar(require("fs"));
|
|
59
61
|
const os = __importStar(require("os"));
|
|
@@ -563,6 +565,7 @@ function agentResolutionFailureMode(command) {
|
|
|
563
565
|
case "setup":
|
|
564
566
|
case "bluebubbles.replay":
|
|
565
567
|
case "bluebubbles.context-smoke":
|
|
568
|
+
case "sync":
|
|
566
569
|
return "throw";
|
|
567
570
|
default:
|
|
568
571
|
return undefined;
|
|
@@ -744,6 +747,10 @@ function writeSyncProbeSummary(deps, findings) {
|
|
|
744
747
|
}
|
|
745
748
|
deps.writeStdout(lines.join("\n"));
|
|
746
749
|
}
|
|
750
|
+
/** What the boot probe saw in each bundle: paused, busy, or commits ahead and behind. */
|
|
751
|
+
function writeSyncProbeStatuses(deps, statuses) {
|
|
752
|
+
deps.writeStdout(["sync status:", ...statuses.map((status) => ` ${status.agent}: ${status.text}`)].join("\n"));
|
|
753
|
+
}
|
|
747
754
|
function bootPhasePlan(_daemonAlive) {
|
|
748
755
|
return ["update check", "system setup", "sync probe", "starting daemon", "provider checks", "final daemon check"];
|
|
749
756
|
}
|
|
@@ -1621,24 +1628,63 @@ function readAgentSyncConfigForCliMutation(agent, deps) {
|
|
|
1621
1628
|
return {
|
|
1622
1629
|
enabled: parsed.sync?.enabled === true,
|
|
1623
1630
|
remote: typeof parsed.sync?.remote === "string" && parsed.sync.remote.trim() ? parsed.sync.remote : "origin",
|
|
1631
|
+
branch: typeof parsed.sync?.branch === "string" && parsed.sync.branch.trim() ? parsed.sync.branch : "main",
|
|
1624
1632
|
};
|
|
1625
1633
|
}
|
|
1626
1634
|
catch {
|
|
1627
1635
|
/* v8 ignore next -- defensive: post-mutation sync should not break an already-successful CLI repair if agent.json becomes unreadable @preserve */
|
|
1628
|
-
return { enabled: false, remote: "origin" };
|
|
1636
|
+
return { enabled: false, remote: "origin", branch: "main" };
|
|
1629
1637
|
}
|
|
1630
1638
|
}
|
|
1631
|
-
|
|
1639
|
+
/** How long a CLI command waits for the bundle sync lock before reporting busy. */
|
|
1640
|
+
const CLI_SYNC_LOCK_WAIT_MS = 60_000;
|
|
1641
|
+
/**
|
|
1642
|
+
* Sync the bundle after a CLI change and report what actually happened. The
|
|
1643
|
+
* sync is awaited: a one-shot CLI that exits explicitly would otherwise kill a
|
|
1644
|
+
* push it had already reported.
|
|
1645
|
+
*/
|
|
1646
|
+
async function pushAgentBundleAfterCliMutation(agent, deps) {
|
|
1632
1647
|
const sync = readAgentSyncConfigForCliMutation(agent, deps);
|
|
1633
1648
|
if (!sync.enabled)
|
|
1634
1649
|
return null;
|
|
1635
1650
|
const agentRoot = providerCliAgentRoot({ agent }, deps);
|
|
1636
|
-
const result = (0, sync_1.
|
|
1651
|
+
const result = await (0, sync_1.syncNow)(agentRoot, sync, agent, deps.syncLockWaitMs ?? CLI_SYNC_LOCK_WAIT_MS);
|
|
1652
|
+
if (result.paused)
|
|
1653
|
+
return `bundle sync: paused until ${result.pausedUntil}; nothing pushed`;
|
|
1637
1654
|
if (result.ok) {
|
|
1638
1655
|
return `bundle sync: ran post-change sync (remote: ${sync.remote})`;
|
|
1639
1656
|
}
|
|
1640
1657
|
return `bundle sync: could not push bundle changes (${result.error})`;
|
|
1641
1658
|
}
|
|
1659
|
+
function bundleHasRemote(agentRoot, remote) {
|
|
1660
|
+
try {
|
|
1661
|
+
return (0, child_process_1.execFileSync)("git", ["remote"], { cwd: agentRoot, stdio: "pipe" }).toString().split("\n").map((line) => line.trim()).includes(remote);
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
/* v8 ignore next -- syncNow just ran git in this bundle; this only words the success line @preserve */
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
/** `ouro sync`: one fetch, merge and push, reported as it happened. Exits 1 unless the bundle synced. */
|
|
1669
|
+
async function executeSync(agent, deps) {
|
|
1670
|
+
const sync = readAgentSyncConfigForCliMutation(agent, deps);
|
|
1671
|
+
if (!sync.enabled)
|
|
1672
|
+
return returnCliFailure(deps, `sync is off for ${agent}: agent.json has no "sync": { "enabled": true }`);
|
|
1673
|
+
const agentRoot = providerCliAgentRoot({ agent }, deps);
|
|
1674
|
+
const result = await (0, sync_1.syncNow)(agentRoot, sync, agent, deps.syncLockWaitMs ?? CLI_SYNC_LOCK_WAIT_MS);
|
|
1675
|
+
if (result.paused)
|
|
1676
|
+
return returnCliFailure(deps, `sync paused until ${result.pausedUntil}`);
|
|
1677
|
+
if (result.busy) {
|
|
1678
|
+
return returnCliFailure(deps, `sync busy: another sync holds the lock${result.holderPid == null ? "" : ` (pid ${result.holderPid})`}`);
|
|
1679
|
+
}
|
|
1680
|
+
if (!result.ok)
|
|
1681
|
+
return returnCliFailure(deps, `sync failed for ${agent}: ${result.error}`);
|
|
1682
|
+
const message = bundleHasRemote(agentRoot, sync.remote)
|
|
1683
|
+
? `synced ${agent}: up to date with ${sync.remote}/${sync.branch}`
|
|
1684
|
+
: `synced ${agent}: committed locally; no remote "${sync.remote}"`;
|
|
1685
|
+
deps.writeStdout(message);
|
|
1686
|
+
return message;
|
|
1687
|
+
}
|
|
1642
1688
|
function writeAgentVaultConfig(agentName, configPath, config, vault) {
|
|
1643
1689
|
const nextConfig = {
|
|
1644
1690
|
...config,
|
|
@@ -1907,7 +1953,7 @@ async function executeVaultCreate(command, deps) {
|
|
|
1907
1953
|
/* v8 ignore next -- defensive: success path assigns store before continuing @preserve */
|
|
1908
1954
|
if (!store)
|
|
1909
1955
|
throw new Error(`vault create failed for ${command.agent}: local unlock material was not saved`);
|
|
1910
|
-
const syncSummary = pushAgentBundleAfterCliMutation(command.agent, deps);
|
|
1956
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(command.agent, deps);
|
|
1911
1957
|
const fallbackLines = [
|
|
1912
1958
|
`vault created for ${command.agent}`,
|
|
1913
1959
|
`vault: ${email} at ${serverUrl}`,
|
|
@@ -1983,7 +2029,7 @@ async function executeVaultReplace(command, deps) {
|
|
|
1983
2029
|
throw new Error(`vault replace failed for ${command.agent}: no vault repair result`);
|
|
1984
2030
|
if (!repair.ok)
|
|
1985
2031
|
return repair.message;
|
|
1986
|
-
const syncSummary = pushAgentBundleAfterCliMutation(command.agent, deps);
|
|
2032
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(command.agent, deps);
|
|
1987
2033
|
const fallbackLines = [
|
|
1988
2034
|
`vault replaced for ${command.agent}`,
|
|
1989
2035
|
`vault: ${email} at ${serverUrl}`,
|
|
@@ -2095,7 +2141,7 @@ async function executeVaultRecover(command, deps) {
|
|
|
2095
2141
|
progress.end();
|
|
2096
2142
|
}
|
|
2097
2143
|
const providerList = [...importedProviders].sort();
|
|
2098
|
-
const syncSummary = pushAgentBundleAfterCliMutation(command.agent, deps);
|
|
2144
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(command.agent, deps);
|
|
2099
2145
|
const fallbackLines = [
|
|
2100
2146
|
`vault recovered for ${command.agent}`,
|
|
2101
2147
|
`vault: ${email} at ${serverUrl}`,
|
|
@@ -2269,13 +2315,13 @@ function currentMachineId(deps) {
|
|
|
2269
2315
|
now: () => providerCliNow(deps),
|
|
2270
2316
|
}).machineId;
|
|
2271
2317
|
}
|
|
2272
|
-
function executeAgentMove(command, deps) {
|
|
2318
|
+
async function executeAgentMove(command, deps) {
|
|
2273
2319
|
const identity = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: providerCliHomeDir(deps), now: () => providerCliNow(deps) });
|
|
2274
2320
|
/* v8 ignore next -- production default: uses real bundles root @preserve */
|
|
2275
2321
|
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
2276
2322
|
const discover = () => (0, agent_discovery_1.listAllBundleAgents)({ bundlesRoot, machineId: identity.machineId });
|
|
2277
2323
|
const fallbackBefore = new Set(discover().filter((row) => row.homeState === "fallback").map((row) => row.name));
|
|
2278
|
-
const result = (0, agent_home_1.moveAgentHomeHere)({
|
|
2324
|
+
const result = await (0, agent_home_1.moveAgentHomeHere)({
|
|
2279
2325
|
agent: command.agent,
|
|
2280
2326
|
machineId: identity.machineId,
|
|
2281
2327
|
machineName: (0, agent_home_1.machineDisplayName)(os.hostname(), identity.machineId),
|
|
@@ -3682,7 +3728,7 @@ async function executeConnectTeams(agent, deps) {
|
|
|
3682
3728
|
finally {
|
|
3683
3729
|
progress.end();
|
|
3684
3730
|
}
|
|
3685
|
-
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
3731
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(agent, deps);
|
|
3686
3732
|
return writeCapabilityOutcome(deps, {
|
|
3687
3733
|
subtitle: `${agent}'s Teams sense is configured.`,
|
|
3688
3734
|
summary: `Teams is ready for ${agent}.`,
|
|
@@ -3910,7 +3956,7 @@ async function executeConnectBlueBubbles(agent, deps) {
|
|
|
3910
3956
|
progress.end();
|
|
3911
3957
|
throw error;
|
|
3912
3958
|
}
|
|
3913
|
-
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
3959
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(agent, deps);
|
|
3914
3960
|
return writeCapabilityOutcome(deps, {
|
|
3915
3961
|
subtitle: `${agent}'s local bridge is attached.`,
|
|
3916
3962
|
summary: `BlueBubbles is attached on this machine for ${agent}.`,
|
|
@@ -4256,7 +4302,7 @@ async function ensureAgentMailroom(agent, input, deps, progressLabel) {
|
|
|
4256
4302
|
/* v8 ignore next -- defensive: successful setup always stores before leaving the guarded block. @preserve */
|
|
4257
4303
|
if (!stored)
|
|
4258
4304
|
throw new Error("Mailroom setup did not store runtime credentials");
|
|
4259
|
-
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
4305
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(agent, deps);
|
|
4260
4306
|
return { mailboxAddress, sourceAlias, mode, registryPath, storePath, hostedControlUrl, blobStoreLabel, stored, syncSummary };
|
|
4261
4307
|
}
|
|
4262
4308
|
async function executeConnectMail(agent, deps, input = {}) {
|
|
@@ -5154,7 +5200,7 @@ async function executeConnectVoice(agent, deps) {
|
|
|
5154
5200
|
async function executeConnectA2A(agent, deps, bind = {}) {
|
|
5155
5201
|
const { defaultA2APort } = await Promise.resolve().then(() => __importStar(require("../../a2a/config")));
|
|
5156
5202
|
enableAgentSense(agent, "a2a", deps);
|
|
5157
|
-
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
5203
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(agent, deps);
|
|
5158
5204
|
// Bind address, port and advertised URL are per machine (D-038): a tailnet IP on one
|
|
5159
5205
|
// host means nothing on another, so they live in this machine's runtime config.
|
|
5160
5206
|
const a2a = {
|
|
@@ -5221,7 +5267,7 @@ async function executeConnectTelegram(agent, deps) {
|
|
|
5221
5267
|
finally {
|
|
5222
5268
|
progress.end();
|
|
5223
5269
|
}
|
|
5224
|
-
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
5270
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(agent, deps);
|
|
5225
5271
|
return writeCapabilityOutcome(deps, {
|
|
5226
5272
|
subtitle: `${agent}'s private Telegram sense is configured.`,
|
|
5227
5273
|
summary: `Telegram is ready for ${agent}.`,
|
|
@@ -5248,7 +5294,7 @@ async function executeConnectTelegram(agent, deps) {
|
|
|
5248
5294
|
async function executeConnectWorkbench(agent, deps) {
|
|
5249
5295
|
const { workbenchMcpCommand, workbenchHasStaleBundleEntry } = readConnectBaySenseFlags(agent, deps);
|
|
5250
5296
|
const changed = removeWorkbenchMcpRegistration(agent, deps);
|
|
5251
|
-
const syncSummary = changed ? pushAgentBundleAfterCliMutation(agent, deps) : "";
|
|
5297
|
+
const syncSummary = changed ? await pushAgentBundleAfterCliMutation(agent, deps) : "";
|
|
5252
5298
|
const executablePath = findInstalledWorkbenchMcp(deps, workbenchMcpCommand);
|
|
5253
5299
|
if (!executablePath) {
|
|
5254
5300
|
throw new Error([
|
|
@@ -5404,8 +5450,8 @@ function writeAgentProviderBinding(input) {
|
|
|
5404
5450
|
meta: { agent: input.agent, lane: input.lane, provider: input.provider, model: input.model, configPath },
|
|
5405
5451
|
});
|
|
5406
5452
|
}
|
|
5407
|
-
function appendBundleSyncSummary(message, agent, deps) {
|
|
5408
|
-
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
5453
|
+
async function appendBundleSyncSummary(message, agent, deps) {
|
|
5454
|
+
const syncSummary = await pushAgentBundleAfterCliMutation(agent, deps);
|
|
5409
5455
|
return syncSummary ? [message, syncSummary].join("\n") : message;
|
|
5410
5456
|
}
|
|
5411
5457
|
async function executeProviderUse(command, deps, options = {}) {
|
|
@@ -5445,7 +5491,7 @@ async function executeProviderUse(command, deps, options = {}) {
|
|
|
5445
5491
|
model: command.model,
|
|
5446
5492
|
deps,
|
|
5447
5493
|
});
|
|
5448
|
-
const message = appendBundleSyncSummary(`forced ${command.agent} ${command.lane} to ${command.provider} / ${command.model}: failed (${credential.error})`, command.agent, deps);
|
|
5494
|
+
const message = await appendBundleSyncSummary(`forced ${command.agent} ${command.lane} to ${command.provider} / ${command.model}: failed (${credential.error})`, command.agent, deps);
|
|
5449
5495
|
return writeMessage(message);
|
|
5450
5496
|
}
|
|
5451
5497
|
progress?.completePhase(`reading ${command.provider} credentials`, "found");
|
|
@@ -5472,7 +5518,7 @@ async function executeProviderUse(command, deps, options = {}) {
|
|
|
5472
5518
|
model: command.model,
|
|
5473
5519
|
deps,
|
|
5474
5520
|
});
|
|
5475
|
-
const message = appendBundleSyncSummary(`${command.force ? "forced " : ""}${command.agent} ${command.lane} ${command.provider} / ${command.model}: ${status}`, command.agent, deps);
|
|
5521
|
+
const message = await appendBundleSyncSummary(`${command.force ? "forced " : ""}${command.agent} ${command.lane} ${command.provider} / ${command.model}: ${status}`, command.agent, deps);
|
|
5476
5522
|
(0, runtime_1.emitNervesEvent)({
|
|
5477
5523
|
component: "daemon",
|
|
5478
5524
|
event: "daemon.provider_use_completed",
|
|
@@ -6003,7 +6049,7 @@ async function executeLegacyConfigModel(command, deps) {
|
|
|
6003
6049
|
},
|
|
6004
6050
|
};
|
|
6005
6051
|
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf-8");
|
|
6006
|
-
const message = appendBundleSyncSummary(`deprecated: updated ${command.agent} model in agent.json on ${lane}/${binding.provider}: ${binding.model} -> ${command.modelName}\nUse \`ouro use --agent ${command.agent} --lane ${lane} --provider ${binding.provider} --model ${command.modelName}\` next time.`, command.agent, deps);
|
|
6052
|
+
const message = await appendBundleSyncSummary(`deprecated: updated ${command.agent} model in agent.json on ${lane}/${binding.provider}: ${binding.model} -> ${command.modelName}\nUse \`ouro use --agent ${command.agent} --lane ${lane} --provider ${binding.provider} --model ${command.modelName}\` next time.`, command.agent, deps);
|
|
6007
6053
|
return writeMessage(message);
|
|
6008
6054
|
}
|
|
6009
6055
|
// ── System setup ──
|
|
@@ -6647,6 +6693,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
6647
6693
|
if (command.kind === "nerves-review") {
|
|
6648
6694
|
return executeNervesReviewCommand(command, deps);
|
|
6649
6695
|
}
|
|
6696
|
+
// Pure: no daemon, no network, no bundle. The cutover runs it in a container with --network none.
|
|
6697
|
+
if (command.kind === "sanctuary.git-exclude") {
|
|
6698
|
+
const text = (0, sanctuary_bundle_migration_1.sanctuaryGitExcludeText)();
|
|
6699
|
+
deps.writeStdout(text);
|
|
6700
|
+
return text;
|
|
6701
|
+
}
|
|
6650
6702
|
if (args.length === 0) {
|
|
6651
6703
|
const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
|
|
6652
6704
|
/* v8 ignore start -- the interactive home shell is exercised extensively in daemon-cli tests; V8 miscounts this orchestrator because it chains through recursive command handoffs and early chat health exits @preserve */
|
|
@@ -7146,6 +7198,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7146
7198
|
if (syncProbeResult.findings.length > 0) {
|
|
7147
7199
|
writeSyncProbeSummary(deps, syncProbeResult.findings);
|
|
7148
7200
|
}
|
|
7201
|
+
if (syncProbeResult.statuses && syncProbeResult.statuses.length > 0) {
|
|
7202
|
+
writeSyncProbeStatuses(deps, syncProbeResult.statuses);
|
|
7203
|
+
}
|
|
7149
7204
|
}
|
|
7150
7205
|
catch (probeError) {
|
|
7151
7206
|
const message = probeError instanceof Error ? probeError.message : String(probeError);
|
|
@@ -9081,6 +9136,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
9081
9136
|
}
|
|
9082
9137
|
return output;
|
|
9083
9138
|
}
|
|
9139
|
+
// ── sync: fetch, merge and push one bundle now ──
|
|
9140
|
+
if (command.kind === "sync") {
|
|
9141
|
+
return executeSync(command.agent, deps);
|
|
9142
|
+
}
|
|
9084
9143
|
// ── clone: clone an agent bundle from a git remote ──
|
|
9085
9144
|
if (command.kind === "clone") {
|
|
9086
9145
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -9149,7 +9208,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
9149
9208
|
}
|
|
9150
9209
|
// 8. Output success message
|
|
9151
9210
|
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_complete", message: "clone complete", meta: { agentName, targetPath } });
|
|
9152
|
-
const syncSummary = syncEnabled ? pushAgentBundleAfterCliMutation(agentName, deps) : null;
|
|
9211
|
+
const syncSummary = syncEnabled ? await pushAgentBundleAfterCliMutation(agentName, deps) : null;
|
|
9153
9212
|
const syncMsg = syncEnabled ? "\nsync enabled (remote: origin)" : "\nwarning: no agent.json found — this may not be a valid agent bundle";
|
|
9154
9213
|
const syncPushMsg = syncSummary ? `\n${syncSummary}` : "";
|
|
9155
9214
|
deps.writeStdout(`cloned ${agentName} to ${targetPath}${syncMsg}${syncPushMsg}`);
|
|
@@ -80,6 +80,12 @@ exports.COMMAND_REGISTRY = {
|
|
|
80
80
|
usage: "ouro clone <remote> [--agent <name>]",
|
|
81
81
|
example: "ouro clone https://github.com/user/myagent.ouro.git",
|
|
82
82
|
},
|
|
83
|
+
sync: {
|
|
84
|
+
category: "Lifecycle",
|
|
85
|
+
description: "Sync an agent bundle with its git remote now: fetch, merge, push",
|
|
86
|
+
usage: "ouro sync [--agent <name>]",
|
|
87
|
+
example: "ouro sync --agent slugger",
|
|
88
|
+
},
|
|
83
89
|
doctor: {
|
|
84
90
|
category: "Lifecycle",
|
|
85
91
|
description: "Run diagnostic checks on the ouro installation",
|
|
@@ -211,6 +217,12 @@ exports.COMMAND_REGISTRY = {
|
|
|
211
217
|
example: "ouro work sentinel --agent my-agent --format json",
|
|
212
218
|
subcommands: ["card", "gauntlet", "sentinel"],
|
|
213
219
|
},
|
|
220
|
+
sanctuary: {
|
|
221
|
+
category: "Internal",
|
|
222
|
+
description: "Print the git exclude list for the Butler's bundle, one entry per line. Starts nothing and reads nothing.",
|
|
223
|
+
usage: "ouro sanctuary git-exclude --print",
|
|
224
|
+
example: "ouro sanctuary git-exclude --print",
|
|
225
|
+
},
|
|
214
226
|
"nerves-review": {
|
|
215
227
|
category: "Internal",
|
|
216
228
|
description: `Read-only review of recent nerves events from one agent log stream (default: ${core_1.DEFAULT_NERVES_PROCESS})`,
|
|
@@ -327,6 +327,7 @@ function usage() {
|
|
|
327
327
|
" ouro rollback [<version>]",
|
|
328
328
|
" ouro versions",
|
|
329
329
|
" ouro clone <remote> [--agent <name>]",
|
|
330
|
+
" ouro sync [--agent <name>]",
|
|
330
331
|
" ouro doctor",
|
|
331
332
|
].join("\n");
|
|
332
333
|
}
|
|
@@ -2163,6 +2164,12 @@ function parseCloneCommand(args) {
|
|
|
2163
2164
|
? { kind: "clone", remote, agent }
|
|
2164
2165
|
: { kind: "clone", remote };
|
|
2165
2166
|
}
|
|
2167
|
+
function parseSyncCommand(args) {
|
|
2168
|
+
const { agent, rest } = extractAgentFlag(args);
|
|
2169
|
+
if (rest.length > 0)
|
|
2170
|
+
throw new Error("Usage: ouro sync [--agent <name>]");
|
|
2171
|
+
return agent ? { kind: "sync", agent } : { kind: "sync" };
|
|
2172
|
+
}
|
|
2166
2173
|
function parseMcpServeCommand(args) {
|
|
2167
2174
|
let agent;
|
|
2168
2175
|
let friendId;
|
|
@@ -3281,6 +3288,11 @@ function parseOuroCommand(args) {
|
|
|
3281
3288
|
return parsePokeCommand(args.slice(1));
|
|
3282
3289
|
if (head === "nerves-review")
|
|
3283
3290
|
return parseNervesReviewCommand(args.slice(1));
|
|
3291
|
+
if (head === "sanctuary") {
|
|
3292
|
+
if (args.length === 3 && args[1] === "git-exclude" && args[2] === "--print")
|
|
3293
|
+
return { kind: "sanctuary.git-exclude" };
|
|
3294
|
+
throw new Error("Usage: ouro sanctuary git-exclude --print");
|
|
3295
|
+
}
|
|
3284
3296
|
if (head === "link")
|
|
3285
3297
|
return parseLinkCommand(args.slice(1));
|
|
3286
3298
|
if (head === "acp-serve")
|
|
@@ -3293,6 +3305,8 @@ function parseOuroCommand(args) {
|
|
|
3293
3305
|
return parsePluginCommand(args.slice(1));
|
|
3294
3306
|
if (head === "clone")
|
|
3295
3307
|
return parseCloneCommand(args.slice(1));
|
|
3308
|
+
if (head === "sync")
|
|
3309
|
+
return parseSyncCommand(args.slice(1));
|
|
3296
3310
|
if (head === "doctor") {
|
|
3297
3311
|
const tail = args.slice(1);
|
|
3298
3312
|
const json = tail.includes("--json");
|
|
@@ -67,7 +67,22 @@ function parseSingleInspect(raw) {
|
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
function
|
|
70
|
+
function splitSyncFlag(args) {
|
|
71
|
+
const index = args.indexOf("--sync-enabled");
|
|
72
|
+
if (index === -1)
|
|
73
|
+
return { args, syncEnabled: false };
|
|
74
|
+
const value = args[index + 1];
|
|
75
|
+
if (value !== "true" && value !== "false")
|
|
76
|
+
return null;
|
|
77
|
+
const rest = [...args.slice(0, index), ...args.slice(index + 2)];
|
|
78
|
+
if (rest.includes("--sync-enabled"))
|
|
79
|
+
return null;
|
|
80
|
+
return { args: rest, syncEnabled: value === "true" };
|
|
81
|
+
}
|
|
82
|
+
function runContainerSpecAuditorCli(rawArgs, deps = {}) {
|
|
83
|
+
const sync = splitSyncFlag(rawArgs);
|
|
84
|
+
const args = sync?.args ?? [];
|
|
85
|
+
const syncEnabled = sync?.syncEnabled === true;
|
|
71
86
|
const readFile = deps.readFile ?? ((filePath) => fs.readFileSync(filePath));
|
|
72
87
|
const readText = (filePath) => decodeUtf8(readFile(filePath));
|
|
73
88
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
@@ -78,14 +93,14 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
78
93
|
const sourceContract = sourceCandidate?.["--mount-contract"];
|
|
79
94
|
const sourceEffective = sourceContract === "legacy-alpha742" ? sourceCandidate : null;
|
|
80
95
|
const selectedEffective = effective ?? sourceEffective;
|
|
81
|
-
if (!staged && !persistent && !selectedEffective) {
|
|
82
|
-
write(JSON.stringify({ ok: false, error: "usage: all modes require --mount-contract canonical-pre-gateway|canonical-gateway; staged --template <path> --runtime-policy <path> --expected-image <id>; persistent --persistent-template <path> --runtime-policy <path> --expected-image-reference <tag>; effective --inspect <path> --image-inspect <path> --expected-image <id> --expected-image-reference <tag> --expected-icon <url>; source compatibility effective --inspect <path> --image-inspect <path> --expected-image <id> --mount-contract legacy-alpha742" }) + "\n");
|
|
96
|
+
if (!sync || (!staged && !persistent && !selectedEffective)) {
|
|
97
|
+
write(JSON.stringify({ ok: false, error: "usage: all modes require --mount-contract canonical-pre-gateway|canonical-gateway and accept --sync-enabled true|false; staged --template <path> --runtime-policy <path> --expected-image <id>; persistent --persistent-template <path> --runtime-policy <path> --expected-image-reference <tag>; effective --inspect <path> --image-inspect <path> --expected-image <id> --expected-image-reference <tag> --expected-icon <url>; source compatibility effective --inspect <path> --image-inspect <path> --expected-image <id> --mount-contract legacy-alpha742" }) + "\n");
|
|
83
98
|
(0, runtime_1.emitNervesEvent)({
|
|
84
99
|
level: "error",
|
|
85
100
|
component: "daemon",
|
|
86
101
|
event: "daemon.container_spec_auditor_cli_error",
|
|
87
102
|
message: "container spec auditor arguments were invalid",
|
|
88
|
-
meta: { argumentCount:
|
|
103
|
+
meta: { argumentCount: rawArgs.length },
|
|
89
104
|
});
|
|
90
105
|
return 2;
|
|
91
106
|
}
|
|
@@ -130,6 +145,7 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
130
145
|
expectedImageReference: selectedEffective["--expected-image-reference"],
|
|
131
146
|
expectedIcon: selectedEffective["--expected-icon"],
|
|
132
147
|
mountContract: selectedEffective["--mount-contract"],
|
|
148
|
+
syncEnabled,
|
|
133
149
|
});
|
|
134
150
|
if (imageInspect.Id !== selectedEffective["--expected-image"]) {
|
|
135
151
|
result.ok = false;
|
|
@@ -159,8 +175,8 @@ function runContainerSpecAuditorCli(args, deps = {}) {
|
|
|
159
175
|
return 2;
|
|
160
176
|
}
|
|
161
177
|
const result = persistent
|
|
162
|
-
? (0, container_spec_auditor_1.auditSanctuaryPersistentTemplate)({ templateXml, runtimePolicyText, expectedImageReference: persistent["--expected-image-reference"], mountContract })
|
|
163
|
-
: (0, container_spec_auditor_1.auditSanctuaryStagedFiles)({ templateXml, runtimePolicyText, expectedImage: staged["--expected-image"], mountContract });
|
|
178
|
+
? (0, container_spec_auditor_1.auditSanctuaryPersistentTemplate)({ templateXml, runtimePolicyText, expectedImageReference: persistent["--expected-image-reference"], mountContract, syncEnabled })
|
|
179
|
+
: (0, container_spec_auditor_1.auditSanctuaryStagedFiles)({ templateXml, runtimePolicyText, expectedImage: staged["--expected-image"], mountContract, syncEnabled });
|
|
164
180
|
write(JSON.stringify(result) + "\n");
|
|
165
181
|
return result.ok ? 0 : 1;
|
|
166
182
|
}
|
|
@@ -15,6 +15,8 @@ const EXPECTED_BINDS = [
|
|
|
15
15
|
"/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/psyche:/home/ouro/AgentBundles/sanctuary.ouro/psyche:ro",
|
|
16
16
|
// Operator-set grants, root-owned on the host and read-only inside the Butler (the Butler honours a grant only from here).
|
|
17
17
|
"/mnt/user/appdata/ouro-butler/trust/sanctuary:/etc/ouro/trust/sanctuary:ro",
|
|
18
|
+
// The bundle sync deploy key, root-owned on the host and read-only inside the Butler. The key can push to the owner's repo.
|
|
19
|
+
"/mnt/user/appdata/ouro-butler/sync:/run/ouro-sync:ro",
|
|
18
20
|
];
|
|
19
21
|
const EXPECTED_MOUNTS = [
|
|
20
22
|
["/mnt/user/appdata/ouro-butler/runtime/.ouro-cli", "/home/ouro/.ouro-cli", true, "rprivate"],
|
|
@@ -23,11 +25,25 @@ const EXPECTED_MOUNTS = [
|
|
|
23
25
|
["/run/ouro-authority", "/run/ouro-authority", false, "rprivate"],
|
|
24
26
|
["/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/psyche", "/home/ouro/AgentBundles/sanctuary.ouro/psyche", false, "rprivate"],
|
|
25
27
|
["/mnt/user/appdata/ouro-butler/trust/sanctuary", "/etc/ouro/trust/sanctuary", false, "rprivate"],
|
|
28
|
+
["/mnt/user/appdata/ouro-butler/sync", "/run/ouro-sync", false, "rprivate"],
|
|
26
29
|
];
|
|
27
30
|
const PSYCHE_TARGET = "/home/ouro/AgentBundles/sanctuary.ouro/psyche";
|
|
28
31
|
/** The current resident shape: the canonical-gateway set without the separate read-only psyche mount. */
|
|
29
32
|
const GATEWAY_BINDS_WITHOUT_PSYCHE = EXPECTED_BINDS.filter((bind) => !bind.includes(`:${PSYCHE_TARGET}:`));
|
|
30
33
|
const GATEWAY_MOUNTS_WITHOUT_PSYCHE = EXPECTED_MOUNTS.filter((mount) => mount[1] !== PSYCHE_TARGET);
|
|
34
|
+
const SYNC_TARGET = "/run/ouro-sync";
|
|
35
|
+
/**
|
|
36
|
+
* The canonical-gateway shapes. With sync on, the key mount is required. With sync off (the live Butler until the
|
|
37
|
+
* cutover, and a rollback to a release without the mount) each shape is accepted with or without it.
|
|
38
|
+
*/
|
|
39
|
+
function gatewayBinds(syncEnabled) {
|
|
40
|
+
const shapes = [EXPECTED_BINDS, GATEWAY_BINDS_WITHOUT_PSYCHE];
|
|
41
|
+
return syncEnabled ? shapes : [...shapes, ...shapes.map((binds) => binds.filter((bind) => !bind.includes(`:${SYNC_TARGET}:`)))];
|
|
42
|
+
}
|
|
43
|
+
function gatewayMounts(syncEnabled) {
|
|
44
|
+
const shapes = [EXPECTED_MOUNTS, GATEWAY_MOUNTS_WITHOUT_PSYCHE];
|
|
45
|
+
return syncEnabled ? shapes : [...shapes, ...shapes.map((mounts) => mounts.filter((mount) => mount[1] !== SYNC_TARGET))];
|
|
46
|
+
}
|
|
31
47
|
const PRE_GATEWAY_BINDS = EXPECTED_BINDS.slice(0, 3);
|
|
32
48
|
const PRE_GATEWAY_MOUNTS = EXPECTED_MOUNTS.slice(0, 3);
|
|
33
49
|
const LEGACY_ALPHA742_IMAGE = "sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d";
|
|
@@ -71,7 +87,7 @@ function auditSanctuaryContainerSpec(value, options) {
|
|
|
71
87
|
violations.push("unsupported mount contract");
|
|
72
88
|
if (mountContract === "legacy-alpha742" && options.expectedImage !== LEGACY_ALPHA742_IMAGE)
|
|
73
89
|
violations.push("legacy mount exception requires the pinned alpha.742 image ID");
|
|
74
|
-
const acceptedMounts = mountContract === "legacy-alpha742" ? [LEGACY_ALPHA742_MOUNTS] : mountContract === "canonical-pre-gateway" ? [PRE_GATEWAY_MOUNTS] :
|
|
90
|
+
const acceptedMounts = mountContract === "legacy-alpha742" ? [LEGACY_ALPHA742_MOUNTS] : mountContract === "canonical-pre-gateway" ? [PRE_GATEWAY_MOUNTS] : gatewayMounts(options.syncEnabled === true);
|
|
75
91
|
const expectedArgs = mountContract !== "legacy-alpha742" ? PACKAGE_DAEMON_ARGS : LEGACY_DAEMON_ARGS;
|
|
76
92
|
const expectedEntrypoint = mountContract !== "legacy-alpha742" ? PACKAGE_ENTRYPOINT : LEGACY_ENTRYPOINT;
|
|
77
93
|
if (root.Image !== options.expectedImage)
|
|
@@ -180,7 +196,7 @@ function auditTemplate(input, expectedRepository, repositoryIsValid, repositoryV
|
|
|
180
196
|
const violations = [];
|
|
181
197
|
if (input.mountContract !== "canonical-pre-gateway" && input.mountContract !== "canonical-gateway")
|
|
182
198
|
violations.push("unsupported template mount contract");
|
|
183
|
-
const acceptedBinds = input.mountContract === "canonical-pre-gateway" ? [PRE_GATEWAY_BINDS] :
|
|
199
|
+
const acceptedBinds = input.mountContract === "canonical-pre-gateway" ? [PRE_GATEWAY_BINDS] : gatewayBinds(input.syncEnabled === true);
|
|
184
200
|
const template = parseDockerManTemplateXml(input.templateXml);
|
|
185
201
|
if (!template)
|
|
186
202
|
violations.push("canonical DockerMan XML structure is invalid");
|
|
@@ -84,6 +84,8 @@ const sanctuary_scheduler_liveness_1 = require("./sanctuary-scheduler-liveness")
|
|
|
84
84
|
const sanctuary_scheduler_origin_1 = require("./sanctuary-scheduler-origin");
|
|
85
85
|
const telegram_1 = require("../../senses/telegram");
|
|
86
86
|
const sync_health_1 = require("../sync-health");
|
|
87
|
+
const sync_1 = require("../sync");
|
|
88
|
+
const sync_lock_1 = require("../sync-lock");
|
|
87
89
|
function parseSocketPath(argv) {
|
|
88
90
|
const socketIndex = argv.indexOf("--socket");
|
|
89
91
|
if (socketIndex >= 0) {
|
|
@@ -701,6 +703,23 @@ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap
|
|
|
701
703
|
supercronicSupervisor?.start();
|
|
702
704
|
const providerPreload = Promise.resolve();
|
|
703
705
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
706
|
+
// The daemon is PID 1 on every container boot, so a lock an earlier boot left looks like ours.
|
|
707
|
+
// Clearing a lock never stops the boot; an unreadable one is judged again by the next sync.
|
|
708
|
+
try {
|
|
709
|
+
for (const row of (0, agent_discovery_1.listBundleSyncRows)({ bundlesRoot })) {
|
|
710
|
+
if (!row.enabled || !row.gitInitialized)
|
|
711
|
+
continue;
|
|
712
|
+
try {
|
|
713
|
+
(0, sync_lock_1.clearOwnStaleSyncLock)(path.join(bundlesRoot, `${row.agent}.ouro`));
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
// Next bundle.
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
// The bundle list could not be read.
|
|
722
|
+
}
|
|
704
723
|
const ouroPath = supercronicSupervisor
|
|
705
724
|
? "/usr/local/bin/node /opt/ouro/dist/heart/daemon/ouro-entry.js"
|
|
706
725
|
: (0, os_cron_deps_1.resolveOuroBinaryPath)();
|
|
@@ -964,6 +983,7 @@ process.on("SIGINT", () => {
|
|
|
964
983
|
// daemon kept dying. Tombstones are informational — having a "sigint"
|
|
965
984
|
// tombstone is strictly better than silence.
|
|
966
985
|
_tombstoneWritten = true;
|
|
986
|
+
(0, sync_1.abortInFlightSync)();
|
|
967
987
|
(0, daemon_tombstone_1.writeDaemonTombstone)("sigint", new Error("daemon received SIGINT"));
|
|
968
988
|
const forcedExit = setTimeout(() => process.exit(1), 12_000);
|
|
969
989
|
forcedExit.unref();
|
|
@@ -977,6 +997,8 @@ process.on("SIGINT", () => {
|
|
|
977
997
|
});
|
|
978
998
|
process.on("SIGTERM", () => {
|
|
979
999
|
_tombstoneWritten = true;
|
|
1000
|
+
// Kill sync's git process groups and release the bundle locks before anything slower.
|
|
1001
|
+
(0, sync_1.abortInFlightSync)();
|
|
980
1002
|
(0, daemon_tombstone_1.writeDaemonTombstone)("sigterm", new Error("daemon received SIGTERM"));
|
|
981
1003
|
const forcedExit = setTimeout(() => process.exit(1), 12_000);
|
|
982
1004
|
forcedExit.unref();
|
|
@@ -60,7 +60,7 @@ function exactSanctuaryContainmentProfileBoundaries(value) {
|
|
|
60
60
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61
61
|
return false;
|
|
62
62
|
const boundaries = value;
|
|
63
|
-
const versions = { "sanctuary-owner":
|
|
63
|
+
const versions = { "sanctuary-owner": 18, "sanctuary-household": 5, "sanctuary-event": 4 };
|
|
64
64
|
if (!(0, node_util_1.isDeepStrictEqual)(Object.keys(boundaries).sort(), Object.keys(versions).sort()))
|
|
65
65
|
return false;
|
|
66
66
|
const packageRoot = path.resolve(__dirname, "../../../deploy/unraid/sanctuary.ouro");
|
|
@@ -33,7 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.SANCTUARY_BUNDLE_ROLLBACK_FILE = exports.SANCTUARY_STARTER_PSYCHE_FILES = exports.SANCTUARY_PACKAGE_MANAGED_FILES = void 0;
|
|
36
|
+
exports.SANCTUARY_GIT_EXCLUDES = exports.SANCTUARY_BUNDLE_ROLLBACK_FILE = exports.SANCTUARY_STARTER_PSYCHE_FILES = exports.SANCTUARY_PACKAGE_MANAGED_FILES = void 0;
|
|
37
|
+
exports.sanctuaryGitExcludeText = sanctuaryGitExcludeText;
|
|
38
|
+
exports.bundleSyncEnabled = bundleSyncEnabled;
|
|
39
|
+
exports.writeSanctuaryGitExclude = writeSanctuaryGitExclude;
|
|
37
40
|
exports.inspectSanctuaryDirectoryFromBase = inspectSanctuaryDirectoryFromBase;
|
|
38
41
|
exports.sanctuaryDirectoriesShareIdentity = sanctuaryDirectoriesShareIdentity;
|
|
39
42
|
exports.inspectSanctuaryPackageManagedBundle = inspectSanctuaryPackageManagedBundle;
|
|
@@ -45,6 +48,7 @@ exports.commitSanctuaryPackageManagedBundle = commitSanctuaryPackageManagedBundl
|
|
|
45
48
|
exports.migrateSanctuaryPackageManagedBundle = migrateSanctuaryPackageManagedBundle;
|
|
46
49
|
const fs = __importStar(require("node:fs"));
|
|
47
50
|
const path = __importStar(require("node:path"));
|
|
51
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
52
|
const node_crypto_1 = require("node:crypto");
|
|
49
53
|
const runtime_1 = require("../../nerves/runtime");
|
|
50
54
|
const house_names_1 = require("../../mind/house-names");
|
|
@@ -64,6 +68,98 @@ const PSYCHE_SEED_STAGE_NAME = /^\.psyche-seed\.[A-Za-z0-9]{6}$/u;
|
|
|
64
68
|
const STARTER_PSYCHE_NAMES = new Set(exports.SANCTUARY_STARTER_PSYCHE_FILES.map((relative) => path.basename(relative)));
|
|
65
69
|
exports.SANCTUARY_BUNDLE_ROLLBACK_FILE = ".sanctuary-package-managed-rollback.json";
|
|
66
70
|
const SANCTUARY_BUNDLE_COMMITTING_FILE = `${exports.SANCTUARY_BUNDLE_ROLLBACK_FILE}.committing`;
|
|
71
|
+
/**
|
|
72
|
+
* What sync never commits from the Butler's bundle. The first block is the hourly host backup's own list, copied from
|
|
73
|
+
* bundle-backup/repo.git/info/exclude on 2026-11-06. Then any MCP credential file, the files each release installs, and
|
|
74
|
+
* the release's own bookkeeping, then the transient temp and lock files written inside the bundle. The cutover writes this list with `ouro sanctuary git-exclude --print`, and every boot
|
|
75
|
+
* writes it again.
|
|
76
|
+
*/
|
|
77
|
+
exports.SANCTUARY_GIT_EXCLUDES = [
|
|
78
|
+
"**/credentials.json",
|
|
79
|
+
"mcp/media-credentials.json",
|
|
80
|
+
"books/hey/.config/",
|
|
81
|
+
"**/*.key",
|
|
82
|
+
"**/*.pem",
|
|
83
|
+
"**/deploy_key*",
|
|
84
|
+
"**/id_rsa*",
|
|
85
|
+
"**/id_ed25519*",
|
|
86
|
+
"**/id_ecdsa*",
|
|
87
|
+
"**/.env",
|
|
88
|
+
"state/",
|
|
89
|
+
"inbox/",
|
|
90
|
+
"arc/flight-recorder/",
|
|
91
|
+
"arc/obligations/",
|
|
92
|
+
"arc/packets/",
|
|
93
|
+
"books/bin/",
|
|
94
|
+
"books/vendor/",
|
|
95
|
+
"books/work/",
|
|
96
|
+
"books/fonts/",
|
|
97
|
+
"mcp/*.pre-*",
|
|
98
|
+
"books/hey/",
|
|
99
|
+
"**/__pycache__/",
|
|
100
|
+
"mcp/*credentials*",
|
|
101
|
+
...exports.SANCTUARY_PACKAGE_MANAGED_FILES,
|
|
102
|
+
"bundle-meta.json",
|
|
103
|
+
`${exports.SANCTUARY_BUNDLE_ROLLBACK_FILE}*`,
|
|
104
|
+
`${PSYCHE_SEED_STAGE_PREFIX}*`,
|
|
105
|
+
// The upgrade's release marker and its staged copy, planted in psyche/ for the length of a gated ship.
|
|
106
|
+
"psyche/.release-marker*",
|
|
107
|
+
// Transient files the runtime and the replay gate write inside the bundle: atomic-write temps and lock files.
|
|
108
|
+
"*.tmp",
|
|
109
|
+
"*.tmp-*",
|
|
110
|
+
"*.tmp.*",
|
|
111
|
+
"*.lock",
|
|
112
|
+
"*.lock.reclaim",
|
|
113
|
+
"*.turn.lock-*",
|
|
114
|
+
".locks/",
|
|
115
|
+
];
|
|
116
|
+
/** The exclude file's content: one entry per line. */
|
|
117
|
+
function sanctuaryGitExcludeText() {
|
|
118
|
+
return `${exports.SANCTUARY_GIT_EXCLUDES.join("\n")}\n`;
|
|
119
|
+
}
|
|
120
|
+
/** Whether the bundle's agent.json turns sync on. An unreadable agent.json reads as off. */
|
|
121
|
+
function bundleSyncEnabled(agentRoot) {
|
|
122
|
+
try {
|
|
123
|
+
const sync = readObject(path.join(agentRoot, "agent.json")).sync;
|
|
124
|
+
return sync?.enabled === true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Write the bundle's `.git/info/exclude`. A bundle with sync on and no repository yet (a fresh install) gets
|
|
132
|
+
* `git init -b main` first; sync makes the first commit on the first turn. A bundle with sync off and no
|
|
133
|
+
* repository is left alone. A tracked .gitignore is the owner's and is never touched.
|
|
134
|
+
*/
|
|
135
|
+
function writeSanctuaryGitExclude(agentRoot) {
|
|
136
|
+
const gitDir = path.join(agentRoot, ".git");
|
|
137
|
+
let initialized = false;
|
|
138
|
+
if (!fs.existsSync(gitDir)) {
|
|
139
|
+
if (!bundleSyncEnabled(agentRoot))
|
|
140
|
+
return { initialized: false, written: false };
|
|
141
|
+
const run = (0, node_child_process_1.spawnSync)("git", ["init", "-q", "-b", "main"], { cwd: agentRoot, env: { ...process.env, LC_ALL: "C", LANG: "C" }, encoding: "utf8" });
|
|
142
|
+
if (run.status !== 0)
|
|
143
|
+
throw new Error(`git init failed: ${run.error ? run.error.message : run.stderr.trim()}`);
|
|
144
|
+
initialized = true;
|
|
145
|
+
}
|
|
146
|
+
const excludePath = path.join(gitDir, "info", "exclude");
|
|
147
|
+
const text = sanctuaryGitExcludeText();
|
|
148
|
+
let current = null;
|
|
149
|
+
try {
|
|
150
|
+
current = fs.readFileSync(excludePath, "utf8");
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!isMissing(error))
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
if (current === text)
|
|
157
|
+
return { initialized, written: false };
|
|
158
|
+
fs.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
159
|
+
writeAtomic(excludePath, text, 0o644);
|
|
160
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.sanctuary_git_exclude_written", message: "wrote the Sanctuary bundle's git exclude", meta: { initialized, entries: exports.SANCTUARY_GIT_EXCLUDES.length } });
|
|
161
|
+
return { initialized, written: true };
|
|
162
|
+
}
|
|
67
163
|
const BUNDLE_META_VERSION_FIELDS = ["runtimeVersion", "bundleSchemaVersion", "lastUpdated"];
|
|
68
164
|
class SanctuaryInspectionFault extends Error {
|
|
69
165
|
code;
|