@ouro.bot/cli 0.1.0-alpha.836 → 0.1.0-alpha.839
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 +19 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +34 -13
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/a2a/onboarding.js +23 -0
- package/dist/heart/daemon/cli-exec.js +53 -6
- package/dist/heart/daemon/cli-help.js +2 -2
- package/dist/heart/daemon/cli-parse.js +72 -23
- package/dist/heart/daemon/sanctuary-authority-installation.js +12 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +21 -3
- package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +2 -3
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.839",
|
|
6
|
+
"changes": [
|
|
7
|
+
"CLI: provision A2A peers without editing files. ouro connect a2a --host/--port/--public-url stores this machine's A2A bind address and advertised URL (D-038); ouro a2a onboard --did <did:key> --name <name> onboards a card-less client such as a coding harness, keyed on its DID; ouro friend update sets --admission, --initiative and --profile as well as --trust (D-039).",
|
|
8
|
+
"CLI: a2a onboard keeps the card and DID paths in one command block so its coverage exemptions stay attached to the right code."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.838",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Upgrade orchestrator: pausing host supervision first installs this version's keeper scripts and raises the shutdown flag every watchdog honours, so an older keeper can no longer restart the resident mid-upgrade (found by the first 830 to 835 rehearsal, which rolled back cleanly). A rehearsal now reports an unplanned failure as a failure instead of as the planned stop."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "0.1.0-alpha.837",
|
|
19
|
+
"changes": [
|
|
20
|
+
"Sanctuary: the root authority keeps a permanent empty child cgroup (ouro-keep), so Unraid's cgroup2-unraid can no longer remove the authority cgroup (D-026). The host keeper now lets the reaper run and pauses it only while the keep child is missing; every reader of the authority cgroup ignores the keep child."
|
|
21
|
+
]
|
|
22
|
+
},
|
|
4
23
|
{
|
|
5
24
|
"version": "0.1.0-alpha.836",
|
|
6
25
|
"changes": [
|
|
@@ -59,6 +59,8 @@ const UPGRADE_JOURNAL = `${ROOT}/upgrade.json`
|
|
|
59
59
|
// Host supervision skips its work while this flag is fresh (< 30 min), so a crashed
|
|
60
60
|
// upgrade can never leave self-heal off for longer than that.
|
|
61
61
|
const MAINTENANCE = "/run/ouro-authority-maintenance"
|
|
62
|
+
// The stop hook's flag: every keeper watchdog version stands down while it exists.
|
|
63
|
+
const SHUTDOWN_FLAG = "/run/ouro-authority-shutdown"
|
|
62
64
|
const UPGRADE_STEPS = ["stop", "switch", "resident", "migrate", "start"]
|
|
63
65
|
|
|
64
66
|
const sh = (file, args, opts = {}) => execFileSync(file, args, { encoding: "utf8", maxBuffer: 64 << 20, ...opts })
|
|
@@ -428,12 +430,13 @@ const GATEWAY_SUPERVISOR = `${AUTHORITY_CUSTOM}/gateway-supervisor.sh`
|
|
|
428
430
|
const GATEWAY_WATCHDOG = `${AUTHORITY_CUSTOM}/gateway-keeper-watchdog.sh`
|
|
429
431
|
const UNRAID_STOP_HOOK = "/boot/config/stop"
|
|
430
432
|
const GATEWAY_SUPERVISOR_SH = String.raw`#!/bin/sh
|
|
431
|
-
# Ouro authority gateway keeper for Unraid
|
|
432
|
-
# The
|
|
433
|
-
#
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
# -
|
|
433
|
+
# Ouro authority gateway keeper for Unraid.
|
|
434
|
+
# The root-authority gateway needs /sys/fs/cgroup/ouro-authority to persist, and Unraid's
|
|
435
|
+
# cgroup2-unraid removes every top-level cgroup that reports populated 0. The kernel will not
|
|
436
|
+
# remove a cgroup that has a child cgroup, so the authority keeps an empty ouro-keep child
|
|
437
|
+
# (D-026). This keeper:
|
|
438
|
+
# - creates that keep child and lets the reaper run; it pauses the reaper only while the
|
|
439
|
+
# keep child is missing (a fresh boot, or an authority older than the keep child),
|
|
437
440
|
# - keeps the gateway process alive,
|
|
438
441
|
# - reconnects the resident (docker restart) if it is unhealthy while the gateway is ready
|
|
439
442
|
# (handles reboot ordering, where Docker autostarts the resident before the gateway).
|
|
@@ -443,15 +446,17 @@ R=/mnt/user/appdata/ouro-authority
|
|
|
443
446
|
GW=$R/package/dist/heart/daemon/sanctuary-telegram-authority-entry.js
|
|
444
447
|
CFG=$R/active.json
|
|
445
448
|
LOG=/var/log/ouro-gateway.log
|
|
446
|
-
|
|
449
|
+
reaper_policy() {
|
|
447
450
|
P=$(cat /run/cgroup2-unraid.pid 2>/dev/null) || return 0
|
|
448
451
|
[ -n "$P" ] || return 0
|
|
452
|
+
if [ -d "$CG/ouro-keep" ]; then kill -CONT "$P" 2>/dev/null; return 0; fi
|
|
449
453
|
S=$(ps -o stat= -p "$P" 2>/dev/null | tr -d ' ')
|
|
450
454
|
case "$S" in T*) : ;; *) kill -STOP "$P" 2>/dev/null ;; esac
|
|
451
455
|
}
|
|
452
456
|
ensure_cg() {
|
|
453
457
|
[ -d "$CG" ] || { mkdir -m 700 "$CG" 2>/dev/null; chown 0:0 "$CG" 2>/dev/null
|
|
454
458
|
for c in cpu memory pids; do echo "+$c" > "$CG/cgroup.subtree_control" 2>/dev/null; done; }
|
|
459
|
+
[ -d "$CG/ouro-keep" ] || mkdir -m 700 "$CG/ouro-keep" 2>/dev/null
|
|
455
460
|
}
|
|
456
461
|
ensure_sock() {
|
|
457
462
|
# Docker autostarts the resident before us and auto-creates its missing bind
|
|
@@ -475,12 +480,12 @@ start_gw() {
|
|
|
475
480
|
}
|
|
476
481
|
# An in-place upgrade pauses us with a flag; a stale flag (> 30 min) is ignored.
|
|
477
482
|
maintenance() { [ -n "$(find /run/ouro-authority-maintenance -mmin -30 2>/dev/null)" ]; }
|
|
478
|
-
|
|
483
|
+
ensure_cg; reaper_policy; ensure_sock
|
|
479
484
|
last_res=0
|
|
480
485
|
while true; do
|
|
481
486
|
if maintenance; then sleep 15; continue; fi
|
|
482
|
-
pause_reaper
|
|
483
487
|
ensure_cg
|
|
488
|
+
reaper_policy
|
|
484
489
|
ensure_sock
|
|
485
490
|
[ -n "$(gw_pid)" ] || { start_gw; sleep 8; }
|
|
486
491
|
rd=$(jq -rc .status "$R"/epochs/*/readiness.json 2>/dev/null)
|
|
@@ -779,13 +784,25 @@ function buildFinalProof(version, out) {
|
|
|
779
784
|
|
|
780
785
|
// ---- upgrade (in place, installed authority) --------------------------------
|
|
781
786
|
|
|
787
|
+
// A host still running an older keeper has a cron watchdog that ignores the maintenance
|
|
788
|
+
// flag: it resurrected the paused supervisor, which restarted the resident mid-upgrade
|
|
789
|
+
// (the first 830 -> 835 rehearsal). So put this version's keeper scripts in place first
|
|
790
|
+
// (they honour the flag) and also raise the shutdown flag every watchdog version honours;
|
|
791
|
+
// resumeSupervision clears both.
|
|
782
792
|
function pauseSupervision() {
|
|
783
793
|
writeFileSync(MAINTENANCE, `${new Date().toISOString()} ${process.pid}\n`)
|
|
794
|
+
writeFileSync(SHUTDOWN_FLAG, `upgrade ${process.pid}\n`)
|
|
795
|
+
for (const [path, body] of [[GATEWAY_SUPERVISOR, GATEWAY_SUPERVISOR_SH], [GATEWAY_WATCHDOG, GATEWAY_WATCHDOG_SH]]) {
|
|
796
|
+
writeFileSync(path, body)
|
|
797
|
+
sh("/bin/chown", ["0:0", path]); chmodSync(path, 0o700)
|
|
798
|
+
}
|
|
784
799
|
sh("/bin/sh", ["-c", "for p in $(pgrep -f '^/bin/sh /boot/config/custom/ouro-authority/[g]ateway-supervisor' || true); do kill $p; done"])
|
|
785
|
-
|
|
800
|
+
if (sh("/bin/sh", ["-c", "pgrep -fc '^/bin/sh /boot/config/custom/ouro-authority/[g]ateway-supervisor' || true"]).trim() !== "0") fail("a gateway supervisor survived the pause")
|
|
801
|
+
ok("host supervision paused (keeper scripts current; flags expire on their own)")
|
|
786
802
|
}
|
|
787
803
|
function resumeSupervision() {
|
|
788
804
|
rmSync(MAINTENANCE, { force: true })
|
|
805
|
+
rmSync(SHUTDOWN_FLAG, { force: true })
|
|
789
806
|
installGatewaySupervisor()
|
|
790
807
|
}
|
|
791
808
|
|
|
@@ -836,8 +853,12 @@ function upgrade(version, rehearse) {
|
|
|
836
853
|
say("lifecycle upgrade (stop → switch → resident → migrate → start)")
|
|
837
854
|
ok(sh("/usr/local/bin/node", [lifecycle, "upgrade", id, image(version), ...(rehearse ? ["--fail-after", rehearse] : [])], { stdio: ["ignore", "pipe", "pipe"] }).trim())
|
|
838
855
|
} catch (e) { failure = e }
|
|
856
|
+
// Only the lifecycle's own rehearsal stop is planned; anything else is a real failure,
|
|
857
|
+
// even during a rehearsal (the first 830 -> 835 rehearsal was mislabelled "as planned").
|
|
858
|
+
const reason = failure ? lifecycleFailure(failure) : ""
|
|
859
|
+
const planned = Boolean(failure && rehearse && reason.includes(`rehearsal stopped after ${rehearse}`))
|
|
839
860
|
if (failure) {
|
|
840
|
-
console.log(`\n!! ${
|
|
861
|
+
console.log(`\n!! ${planned ? "rehearsal stopped as planned" : "upgrade FAILED"}: ${reason}\n!! rolling back to the predecessor`)
|
|
841
862
|
try { ok(`rollback: ${sh("/usr/local/bin/node", [lifecycle, "upgrade-rollback"], { stdio: ["ignore", "pipe", "pipe"] }).trim()}`) }
|
|
842
863
|
catch (e) {
|
|
843
864
|
resumeSupervision()
|
|
@@ -850,8 +871,8 @@ function upgrade(version, rehearse) {
|
|
|
850
871
|
sha12(POLICY) === policyBefore ? ok("steward policy unchanged") : fail("STEWARD POLICY CHANGED")
|
|
851
872
|
const live = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}} {{.State.Status}}/{{.State.Health.Status}}"]).trim()
|
|
852
873
|
ok(`butler: ${live}`)
|
|
853
|
-
if (failure && !
|
|
854
|
-
console.log(rehearse ? "\nREHEARSAL done — the rollback restored the predecessor." : `\nUPGRADE done — ${version} live. Run verify.`)
|
|
874
|
+
if (failure && !planned) fail("upgrade rolled back; the Butler is on its prior version (see above)")
|
|
875
|
+
console.log(rehearse ? "\nREHEARSAL done — the upgrade reached the planned step and the rollback restored the predecessor." : `\nUPGRADE done — ${version} live. Run verify.`)
|
|
855
876
|
}
|
|
856
877
|
|
|
857
878
|
// ---- verify ---------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.839</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
package/dist/a2a/onboarding.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.onboardA2APeer = onboardA2APeer;
|
|
37
|
+
exports.onboardA2AClientByDid = onboardA2AClientByDid;
|
|
37
38
|
const path = __importStar(require("node:path"));
|
|
38
39
|
const identity_1 = require("../heart/identity");
|
|
39
40
|
const runtime_1 = require("../nerves/runtime");
|
|
@@ -108,3 +109,25 @@ async function onboardA2APeer(options) {
|
|
|
108
109
|
});
|
|
109
110
|
return record;
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Onboard a client that serves no agent card (for example a coding harness with a
|
|
114
|
+
* file-backed identity) by its did:key alone. The record is keyed on the DID, so its
|
|
115
|
+
* signed, sealed messages resolve to this friend; the key must parse, or nothing is written.
|
|
116
|
+
*/
|
|
117
|
+
async function onboardA2AClientByDid(options) {
|
|
118
|
+
if ((0, a2a_client_1.parseDidKey)(options.did) === null)
|
|
119
|
+
throw new Error(`not a valid did:key: ${options.did}`);
|
|
120
|
+
const record = await (0, friends_1.upsertAgentPeer)(storeFor(options), {
|
|
121
|
+
name: options.name,
|
|
122
|
+
agentId: options.did,
|
|
123
|
+
...(options.trustLevel ? { trustLevel: options.trustLevel } : {}),
|
|
124
|
+
a2a: { did: options.did, agentId: options.did },
|
|
125
|
+
});
|
|
126
|
+
(0, runtime_1.emitNervesEvent)({
|
|
127
|
+
component: "friends",
|
|
128
|
+
event: "friends.a2a_client_onboarded",
|
|
129
|
+
message: "onboarded card-less A2A client into friend model",
|
|
130
|
+
meta: { agentName: options.agentName, friendId: record.id, peerName: record.name, trustLevel: record.trustLevel, did: options.did },
|
|
131
|
+
});
|
|
132
|
+
return record;
|
|
133
|
+
}
|
|
@@ -5053,13 +5053,23 @@ async function executeConnectVoice(agent, deps) {
|
|
|
5053
5053
|
deps.writeStdout(message);
|
|
5054
5054
|
return message;
|
|
5055
5055
|
}
|
|
5056
|
-
async function executeConnectA2A(agent, deps) {
|
|
5056
|
+
async function executeConnectA2A(agent, deps, bind = {}) {
|
|
5057
5057
|
const { defaultA2APort } = await Promise.resolve().then(() => __importStar(require("../../a2a/config")));
|
|
5058
5058
|
enableAgentSense(agent, "a2a", deps);
|
|
5059
5059
|
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
5060
|
-
|
|
5060
|
+
// Bind address, port and advertised URL are per machine (D-038): a tailnet IP on one
|
|
5061
|
+
// host means nothing on another, so they live in this machine's runtime config.
|
|
5062
|
+
const a2a = {
|
|
5063
|
+
...(bind.host !== undefined ? { host: bind.host } : {}),
|
|
5064
|
+
...(bind.port !== undefined ? { port: bind.port } : {}),
|
|
5065
|
+
...(bind.publicUrl !== undefined ? { publicUrl: bind.publicUrl } : {}),
|
|
5066
|
+
};
|
|
5067
|
+
if (Object.keys(a2a).length > 0)
|
|
5068
|
+
await (0, runtime_credentials_1.mergeMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { a2a }, providerCliNow(deps));
|
|
5069
|
+
const port = bind.port ?? defaultA2APort(agent);
|
|
5061
5070
|
const message = [
|
|
5062
5071
|
`A2A connected for ${agent}`,
|
|
5072
|
+
...(Object.keys(a2a).length > 0 ? [`This machine's A2A listener: ${bind.host ?? "127.0.0.1"}:${port}${bind.publicUrl ? `, advertised as ${bind.publicUrl}` : ""} (applies on the next \`ouro up\`).`] : []),
|
|
5063
5073
|
`The daemon-managed A2A sense will listen locally on port ${port} after \`ouro up\`.`,
|
|
5064
5074
|
`Local agent card: http://127.0.0.1:${port}/.well-known/agent-card.json`,
|
|
5065
5075
|
`Local JSON-RPC endpoint: http://127.0.0.1:${port}/a2a`,
|
|
@@ -5184,7 +5194,7 @@ async function executeConnect(command, deps) {
|
|
|
5184
5194
|
if (command.target === "voice")
|
|
5185
5195
|
return executeConnectVoice(command.agent, deps);
|
|
5186
5196
|
if (command.target === "a2a")
|
|
5187
|
-
return executeConnectA2A(command.agent, deps);
|
|
5197
|
+
return executeConnectA2A(command.agent, deps, { host: command.a2aHost, port: command.a2aPort, publicUrl: command.a2aPublicUrl });
|
|
5188
5198
|
if (command.target === "telegram")
|
|
5189
5199
|
return executeConnectTelegram(command.agent, deps);
|
|
5190
5200
|
if (command.target === "workbench")
|
|
@@ -6034,10 +6044,26 @@ async function executeFriendCommand(command, store) {
|
|
|
6034
6044
|
return `created: ${id} (${command.name}, ${trustLevel})`;
|
|
6035
6045
|
}
|
|
6036
6046
|
if (command.kind === "friend.update") {
|
|
6037
|
-
|
|
6038
|
-
if (result.status === "not_found")
|
|
6047
|
+
if (!await store.get(command.friendId))
|
|
6039
6048
|
return `friend not found: ${command.friendId}`;
|
|
6040
|
-
|
|
6049
|
+
if (command.trustLevel)
|
|
6050
|
+
await (0, friends_1.setFriendTrust)(store, command.friendId, command.trustLevel);
|
|
6051
|
+
const relationship = {
|
|
6052
|
+
...(command.admissionState ? { admissionState: command.admissionState } : {}),
|
|
6053
|
+
...(command.initiativePolicy ? { initiativePolicy: command.initiativePolicy } : {}),
|
|
6054
|
+
...(command.capabilityProfileId ? { capabilityProfileId: command.capabilityProfileId } : {}),
|
|
6055
|
+
};
|
|
6056
|
+
if (Object.keys(relationship).length > 0) {
|
|
6057
|
+
const current = (await store.get(command.friendId));
|
|
6058
|
+
await store.put(command.friendId, { ...current, ...relationship, updatedAt: new Date().toISOString() });
|
|
6059
|
+
}
|
|
6060
|
+
const changed = [
|
|
6061
|
+
command.trustLevel ? `trust=${command.trustLevel}` : "",
|
|
6062
|
+
command.admissionState ? `admission=${command.admissionState}` : "",
|
|
6063
|
+
command.initiativePolicy ? `initiative=${command.initiativePolicy}` : "",
|
|
6064
|
+
command.capabilityProfileId ? `profile=${command.capabilityProfileId}` : "",
|
|
6065
|
+
].filter(Boolean).join(", ");
|
|
6066
|
+
return `updated: ${command.friendId} → ${changed}`;
|
|
6041
6067
|
}
|
|
6042
6068
|
if (command.kind === "friend.link") {
|
|
6043
6069
|
const result = await (0, friends_1.linkExternalId)(store, command.friendId, {
|
|
@@ -6107,6 +6133,27 @@ async function executeA2ACommand(command, deps) {
|
|
|
6107
6133
|
}
|
|
6108
6134
|
/* v8 ignore next -- false branch is foreground serve, intentionally ignored below because it waits for signals @preserve */
|
|
6109
6135
|
if (command.kind === "a2a.onboard") {
|
|
6136
|
+
if (command.did) {
|
|
6137
|
+
const { onboardA2AClientByDid } = await Promise.resolve().then(() => __importStar(require("../../a2a/onboarding")));
|
|
6138
|
+
const record = await onboardA2AClientByDid({
|
|
6139
|
+
agentName: command.agent,
|
|
6140
|
+
did: command.did,
|
|
6141
|
+
name: command.name,
|
|
6142
|
+
...(command.trustLevel ? { trustLevel: command.trustLevel } : {}),
|
|
6143
|
+
/* v8 ignore next -- production CLI falls back to the canonical bundle root; tests inject isolated roots @preserve */
|
|
6144
|
+
...(deps.bundlesRoot ? { bundlesRoot: deps.bundlesRoot } : {}),
|
|
6145
|
+
});
|
|
6146
|
+
const message = [
|
|
6147
|
+
`onboarded A2A client: ${record.name}`,
|
|
6148
|
+
`friend id: ${record.id}`,
|
|
6149
|
+
/* v8 ignore next -- upsertAgentPeer always writes an explicit TrustLevel @preserve */
|
|
6150
|
+
`trust: ${record.trustLevel ?? "unknown"}`,
|
|
6151
|
+
`did: ${command.did}`,
|
|
6152
|
+
`Next: ouro friend update ${record.id} --agent ${command.agent} --admission active --initiative reactive_only --profile <capability-profile-id>`,
|
|
6153
|
+
].join("\n");
|
|
6154
|
+
deps.writeStdout(message);
|
|
6155
|
+
return message;
|
|
6156
|
+
}
|
|
6110
6157
|
const { onboardA2APeer } = await Promise.resolve().then(() => __importStar(require("../../a2a/onboarding")));
|
|
6111
6158
|
const record = await onboardA2APeer({
|
|
6112
6159
|
agentName: command.agent,
|
|
@@ -448,7 +448,7 @@ const SUBCOMMAND_HELP = {
|
|
|
448
448
|
},
|
|
449
449
|
"connect a2a": {
|
|
450
450
|
description: "Enable the agent-to-agent A2A sense",
|
|
451
|
-
usage: "ouro connect a2a [--agent <name>]",
|
|
451
|
+
usage: "ouro connect a2a [--agent <name>] [--host <address>] [--port <port>] [--public-url <url>]",
|
|
452
452
|
example: "ouro connect a2a --agent <agent>",
|
|
453
453
|
},
|
|
454
454
|
"connect telegram": {
|
|
@@ -468,7 +468,7 @@ const SUBCOMMAND_HELP = {
|
|
|
468
468
|
},
|
|
469
469
|
"a2a onboard": {
|
|
470
470
|
description: "Onboard an A2A peer into the existing friend model",
|
|
471
|
-
usage: "ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]",
|
|
471
|
+
usage: "ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]",
|
|
472
472
|
example: "ouro a2a onboard --agent <agent> --card-url https://peer.example/.well-known/agent-card.json --trust friend",
|
|
473
473
|
},
|
|
474
474
|
"a2a serve": {
|
|
@@ -141,7 +141,7 @@ function usage() {
|
|
|
141
141
|
" ouro friend list [--agent <name>]",
|
|
142
142
|
" ouro friend show <id> [--agent <name>]",
|
|
143
143
|
" ouro friend create --name <name> [--trust <level>] [--agent <name>]",
|
|
144
|
-
" ouro friend update <id> --trust <level> [--agent <name>]",
|
|
144
|
+
" ouro friend update <id> [--trust <level>] [--admission <state>] [--initiative <policy>] [--profile <id>] [--agent <name>]",
|
|
145
145
|
" ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
|
|
146
146
|
" ouro private decisions [--agent <name>] [--limit <n>] [--json]",
|
|
147
147
|
" ouro private status [--agent <name>] [--json]",
|
|
@@ -150,7 +150,7 @@ function usage() {
|
|
|
150
150
|
" ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
151
151
|
" ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
152
152
|
" ouro a2a card [--agent <name>] [--base-url <url>] [--json]",
|
|
153
|
-
" ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]",
|
|
153
|
+
" ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]",
|
|
154
154
|
" ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
|
|
155
155
|
" ouro a2a identity [--identity-file <path>] [--json]",
|
|
156
156
|
" ouro a2a message --to <card-url> --text <text> [--context <id>] [--identity-file <path>] [--json]",
|
|
@@ -1099,20 +1099,58 @@ function extractMailSourceFlags(args, usageText) {
|
|
|
1099
1099
|
hasMailSourceFlags,
|
|
1100
1100
|
};
|
|
1101
1101
|
}
|
|
1102
|
+
// `ouro connect a2a` bind flags: where the listener binds and the URL its card advertises (D-038).
|
|
1103
|
+
function extractA2AConnectFlags(args) {
|
|
1104
|
+
const rest = [];
|
|
1105
|
+
const flags = {};
|
|
1106
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1107
|
+
const value = args[i + 1];
|
|
1108
|
+
if (args[i] === "--host" && value) {
|
|
1109
|
+
flags.host = value;
|
|
1110
|
+
i += 1;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (args[i] === "--public-url" && value) {
|
|
1114
|
+
const url = new URL(value);
|
|
1115
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
1116
|
+
throw new Error("A2A public URL must be http(s)");
|
|
1117
|
+
flags.publicUrl = value.replace(/\/+$/u, "");
|
|
1118
|
+
i += 1;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (args[i] === "--port" && value) {
|
|
1122
|
+
const port = Number(value);
|
|
1123
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1124
|
+
throw new Error("A2A port must be 1-65535");
|
|
1125
|
+
flags.port = port;
|
|
1126
|
+
i += 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
rest.push(args[i]);
|
|
1130
|
+
}
|
|
1131
|
+
return { rest, ...flags };
|
|
1132
|
+
}
|
|
1102
1133
|
function parseConnectCommand(args) {
|
|
1103
|
-
const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
|
|
1134
|
+
const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|telegram|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys] [a2a: --host <address> --port <port> --public-url <url>]";
|
|
1104
1135
|
const { agent, rest: afterAgent } = extractAgentFlag(args);
|
|
1105
|
-
const
|
|
1136
|
+
const a2aFlags = extractA2AConnectFlags(afterAgent);
|
|
1137
|
+
const mailFlags = extractMailSourceFlags(a2aFlags.rest, usageText);
|
|
1106
1138
|
if (mailFlags.rest.length > 1)
|
|
1107
1139
|
throw new Error(usageText);
|
|
1108
1140
|
const target = normalizeConnectTarget(mailFlags.rest[0]);
|
|
1109
1141
|
if (mailFlags.hasMailSourceFlags && target !== "mail") {
|
|
1110
1142
|
throw new Error("Mail source flags require `ouro connect mail`.");
|
|
1111
1143
|
}
|
|
1144
|
+
const hasA2AFlags = a2aFlags.host !== undefined || a2aFlags.port !== undefined || a2aFlags.publicUrl !== undefined;
|
|
1145
|
+
if (hasA2AFlags && target !== "a2a")
|
|
1146
|
+
throw new Error("--host, --port and --public-url require `ouro connect a2a`.");
|
|
1112
1147
|
return {
|
|
1113
1148
|
kind: "connect",
|
|
1114
1149
|
...(agent ? { agent } : {}),
|
|
1115
1150
|
...(target ? { target } : {}),
|
|
1151
|
+
...(a2aFlags.host !== undefined ? { a2aHost: a2aFlags.host } : {}),
|
|
1152
|
+
...(a2aFlags.port !== undefined ? { a2aPort: a2aFlags.port } : {}),
|
|
1153
|
+
...(a2aFlags.publicUrl !== undefined ? { a2aPublicUrl: a2aFlags.publicUrl } : {}),
|
|
1116
1154
|
...(mailFlags.ownerEmail !== undefined ? { ownerEmail: mailFlags.ownerEmail } : {}),
|
|
1117
1155
|
...(mailFlags.source !== undefined ? { source: mailFlags.source } : {}),
|
|
1118
1156
|
...(mailFlags.noDelegatedSource ? { noDelegatedSource: true } : {}),
|
|
@@ -1405,25 +1443,30 @@ function parseFriendCommand(args) {
|
|
|
1405
1443
|
}
|
|
1406
1444
|
if (sub === "update") {
|
|
1407
1445
|
const friendId = rest[0];
|
|
1446
|
+
const usage = "Usage: ouro friend update <id> [--trust <stranger|acquaintance|friend|family>] [--admission <unverified|active|revoked>] [--initiative <none|reactive_only|request_follow_up_only|proactive>] [--profile <capability-profile-id>]";
|
|
1408
1447
|
if (!friendId)
|
|
1409
|
-
throw new Error(
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1448
|
+
throw new Error(usage);
|
|
1449
|
+
// Every relationship field an owner provisions, not just trust (D-039).
|
|
1450
|
+
const choices = {
|
|
1451
|
+
"--trust": { key: "trustLevel", values: ["stranger", "acquaintance", "friend", "family"] },
|
|
1452
|
+
"--admission": { key: "admissionState", values: ["unverified", "active", "revoked"] },
|
|
1453
|
+
"--initiative": { key: "initiativePolicy", values: ["none", "reactive_only", "request_follow_up_only", "proactive"] },
|
|
1454
|
+
"--profile": { key: "capabilityProfileId" },
|
|
1455
|
+
};
|
|
1456
|
+
const fields = {};
|
|
1457
|
+
for (let i = 1; i < rest.length; i += 2) {
|
|
1458
|
+
const choice = choices[rest[i]];
|
|
1459
|
+
const value = rest[i + 1];
|
|
1460
|
+
if (!choice || !value || (choice.values && !choice.values.includes(value)))
|
|
1461
|
+
throw new Error(usage);
|
|
1462
|
+
fields[choice.key] = value;
|
|
1422
1463
|
}
|
|
1464
|
+
if (Object.keys(fields).length === 0)
|
|
1465
|
+
throw new Error(usage);
|
|
1423
1466
|
return {
|
|
1424
1467
|
kind: "friend.update",
|
|
1425
1468
|
friendId,
|
|
1426
|
-
|
|
1469
|
+
...fields,
|
|
1427
1470
|
...(agent ? { agent } : {}),
|
|
1428
1471
|
};
|
|
1429
1472
|
}
|
|
@@ -1504,6 +1547,7 @@ function parseA2ACommand(args) {
|
|
|
1504
1547
|
}
|
|
1505
1548
|
if (sub === "onboard") {
|
|
1506
1549
|
let cardUrl;
|
|
1550
|
+
let did;
|
|
1507
1551
|
let trustLevel;
|
|
1508
1552
|
let name;
|
|
1509
1553
|
const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
|
|
@@ -1512,10 +1556,14 @@ function parseA2ACommand(args) {
|
|
|
1512
1556
|
cardUrl = rest[++i];
|
|
1513
1557
|
continue;
|
|
1514
1558
|
}
|
|
1559
|
+
if (rest[i] === "--did" && rest[i + 1]) {
|
|
1560
|
+
did = rest[++i];
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1515
1563
|
if (rest[i] === "--trust" && rest[i + 1]) {
|
|
1516
1564
|
const raw = rest[++i];
|
|
1517
1565
|
if (!VALID_TRUST_LEVELS.has(raw))
|
|
1518
|
-
throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <stranger|acquaintance|friend|family>] [--name <name>]");
|
|
1566
|
+
throw new Error("Usage: ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <stranger|acquaintance|friend|family>] [--name <name>]");
|
|
1519
1567
|
trustLevel = raw;
|
|
1520
1568
|
continue;
|
|
1521
1569
|
}
|
|
@@ -1523,11 +1571,12 @@ function parseA2ACommand(args) {
|
|
|
1523
1571
|
name = rest[++i];
|
|
1524
1572
|
continue;
|
|
1525
1573
|
}
|
|
1526
|
-
throw new Error("Usage: ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]");
|
|
1574
|
+
throw new Error("Usage: ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]");
|
|
1527
1575
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1576
|
+
// A client that serves no card (a coding harness) is onboarded by its did:key alone.
|
|
1577
|
+
if (!cardUrl === !did || (did && !name))
|
|
1578
|
+
throw new Error("Usage: ouro a2a onboard [--agent <name>] (--card-url <url>|--did <did:key> --name <name>) [--trust <level>] [--name <name>]");
|
|
1579
|
+
return { kind: "a2a.onboard", ...(cardUrl ? { cardUrl } : { did: did }), ...(agent ? { agent } : {}), ...(trustLevel ? { trustLevel } : {}), ...(name ? { name } : {}) };
|
|
1531
1580
|
}
|
|
1532
1581
|
if (sub === "serve") {
|
|
1533
1582
|
let host;
|
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SANCTUARY_CGROUP_KEEP = void 0;
|
|
37
|
+
exports.sanctuaryCgroupChildren = sanctuaryCgroupChildren;
|
|
36
38
|
exports.verifySanctuaryAuthorityInstallation = verifySanctuaryAuthorityInstallation;
|
|
37
39
|
const node_crypto_1 = require("node:crypto");
|
|
38
40
|
const fs = __importStar(require("node:fs"));
|
|
@@ -70,6 +72,16 @@ function readOwned(filePath, uid, gid, mode) {
|
|
|
70
72
|
fs.closeSync(descriptor);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Unraid's cgroup2-unraid removes every top-level cgroup that reports `populated 0`,
|
|
77
|
+
* and the kernel refuses to remove a cgroup that has child cgroups. So the authority
|
|
78
|
+
* keeps one permanent empty child, and every reader of its cgroup ignores it (D-026).
|
|
79
|
+
*/
|
|
80
|
+
exports.SANCTUARY_CGROUP_KEEP = "ouro-keep";
|
|
81
|
+
/** The authority cgroup's execution children: directories other than the keep child. */
|
|
82
|
+
function sanctuaryCgroupChildren(cgroupRoot) {
|
|
83
|
+
return fs.readdirSync(cgroupRoot).filter((name) => name !== exports.SANCTUARY_CGROUP_KEEP && fs.lstatSync(path.join(cgroupRoot, name)).isDirectory());
|
|
84
|
+
}
|
|
73
85
|
function verifySanctuaryAuthorityInstallation(input) {
|
|
74
86
|
const { expectedUid: uid, expectedGid: gid } = input;
|
|
75
87
|
for (const root of [input.packageRoot, input.stateRoot, input.stagingRoot, input.cgroupRoot])
|
|
@@ -151,7 +151,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
151
151
|
throw new Error("Resolve pending execution state before re-pinning");
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
if (
|
|
154
|
+
if ((0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
|
|
155
155
|
throw new Error("Resolve pending cgroups before re-pinning");
|
|
156
156
|
this.#verifyPrimitive("/usr/bin/prlimit", prlimitDigest);
|
|
157
157
|
this.#verifyPrimitive("/usr/bin/setsid", setsidDigest);
|
|
@@ -294,9 +294,26 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
294
294
|
throw new Error("Sanctuary installed boot lifecycle changed");
|
|
295
295
|
return true;
|
|
296
296
|
}
|
|
297
|
+
/** Create the authority cgroup with its keep child. The reaper can remove a freshly
|
|
298
|
+
* made (still empty) cgroup before the child exists, so retry that race a few times. */
|
|
299
|
+
#cgroup() {
|
|
300
|
+
const keep = this.#p(`${CGROUP}/${sanctuary_authority_installation_1.SANCTUARY_CGROUP_KEEP}`);
|
|
301
|
+
for (let attempt = 1; !fs.existsSync(keep); attempt += 1) {
|
|
302
|
+
try {
|
|
303
|
+
this.#directory(CGROUP);
|
|
304
|
+
fs.mkdirSync(keep, { mode: 0o700 });
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error.code !== "ENOENT" || attempt >= 5)
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this.#directory(CGROUP);
|
|
312
|
+
}
|
|
297
313
|
#runtimeDirectories() {
|
|
298
|
-
for (const directory of [this.#epochRoot(), STAGING
|
|
314
|
+
for (const directory of [this.#epochRoot(), STAGING])
|
|
299
315
|
this.#directory(directory);
|
|
316
|
+
this.#cgroup();
|
|
300
317
|
this.#directory(SOCKET, 0o750, this.#socketGid);
|
|
301
318
|
// Add only the required controllers; never replace another owner's controls.
|
|
302
319
|
for (const root of ["/sys/fs/cgroup", CGROUP]) {
|
|
@@ -546,7 +563,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
546
563
|
const epoch = this.#epoch();
|
|
547
564
|
if (proof.schemaVersion !== 1 || proof.keyId !== epoch.epochId || proof.publicKeyDigest !== epoch.publicKeyDigest || proof.quiescent !== true
|
|
548
565
|
|| !Number.isSafeInteger(proof.cursor) || proof.cursor < epoch.predecessorCursor
|
|
549
|
-
||
|
|
566
|
+
|| (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#p(CGROUP)).length !== 0)
|
|
550
567
|
throw new Error("Sanctuary gateway retirement cleanup is unproven");
|
|
551
568
|
return true;
|
|
552
569
|
}
|
|
@@ -854,6 +871,7 @@ class SanctuaryAuthorityRootLifecycle {
|
|
|
854
871
|
}
|
|
855
872
|
/** Start the gateway, then the resident, and prove both. */
|
|
856
873
|
async #startUpgraded() {
|
|
874
|
+
this.#runtimeDirectories();
|
|
857
875
|
await this.#startGateway();
|
|
858
876
|
this.#docker(["start", "ouro-butler"]);
|
|
859
877
|
await this.#wait(async () => this.#target().State.Running && this.#target().State.Health?.Status === "healthy" && await this.#ready(), 300_000);
|
|
@@ -39,6 +39,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
39
39
|
const fs = __importStar(require("node:fs"));
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
|
+
const sanctuary_authority_installation_1 = require("./sanctuary-authority-installation");
|
|
42
43
|
const SCHEMA_VERSION = 1;
|
|
43
44
|
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
44
45
|
const PERMIT_ID = /^permit-[A-Za-z0-9_-]{43}$/u;
|
|
@@ -183,10 +184,8 @@ class DetachedSanctuaryHostSupervisor {
|
|
|
183
184
|
throw new Error("Sanctuary host supervisor state is unmatched");
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
|
-
for (const permitId of
|
|
187
|
+
for (const permitId of (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(this.#options.cgroupRoot)) {
|
|
187
188
|
const cgroupPath = path.join(this.#options.cgroupRoot, permitId);
|
|
188
|
-
if (!fs.lstatSync(cgroupPath).isDirectory())
|
|
189
|
-
continue;
|
|
190
189
|
if (!PERMIT_ID.test(permitId))
|
|
191
190
|
throw new Error("Sanctuary host cgroup entry is invalid");
|
|
192
191
|
if (!known.has(permitId) && !await this.#reconcileCgroup(cgroupPath)) {
|
|
@@ -346,7 +346,7 @@ async function startSanctuaryTelegramAuthority(options) {
|
|
|
346
346
|
hostAuthority.retireRegistrations(ledger);
|
|
347
347
|
await reconcileExecutions();
|
|
348
348
|
if (hostAuthority.retireRegistrations(ledger).length !== 0
|
|
349
|
-
||
|
|
349
|
+
|| (0, sanctuary_authority_installation_1.sanctuaryCgroupChildren)(config.hostCgroupRoot).length !== 0)
|
|
350
350
|
throw new Error("Sanctuary authority retirement cleanup is unproven");
|
|
351
351
|
writePrivateJson(path.join(config.epochRoot, "retirement.json"), {
|
|
352
352
|
schemaVersion: 1, keyId: config.keyId, publicKeyDigest: config.publicKeyDigest,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.839",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.839",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|