@ouro.bot/cli 0.1.0-alpha.990 → 0.1.0-alpha.992
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 +12 -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-authority-root-lifecycle.js +21 -5
- 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
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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.992",
|
|
6
|
+
"changes": [
|
|
7
|
+
"The Butler's container keeps its bundle sync key mount through a release: the root authority's activation, which recreates the container after the upgrade, now mounts the key folder read-only as the upgrade does, and a rollback gives it back only to a release that has bundle sync."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.991",
|
|
12
|
+
"changes": [
|
|
13
|
+
"The Butler's home now syncs to its own private GitHub repository: the bundle carries its own git, pushes through a deploy key it can never read, holds its first push for the owner's yes, and the host's hourly backup hands over through a cutover that can always roll back."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.990",
|
|
6
18
|
"changes": [
|
package/deploy/unraid/Dockerfile
CHANGED
|
@@ -7,14 +7,16 @@ ARG TARGETARCH
|
|
|
7
7
|
ARG SUPERCRONIC_AMD64_SHA256=5adff01c5a797663948e656d2b61d10932369ee437eb5cb54fa872b2960f222b
|
|
8
8
|
ARG SUPERCRONIC_ARM64_SHA256=c0576a8eb092e3f79108ed0a2155a25c7766af78456e5a6070e54757ef513bfe
|
|
9
9
|
RUN apt-get update \
|
|
10
|
-
&& apt-get install -y --no-install-recommends ca-certificates curl procps python3 make g++ \
|
|
10
|
+
&& apt-get install -y --no-install-recommends ca-certificates curl procps python3 make g++ git openssh-client sudo \
|
|
11
11
|
&& rm -rf /var/lib/apt/lists/* \
|
|
12
12
|
&& case "$TARGETARCH" in amd64) SUPERCRONIC_SHA256="$SUPERCRONIC_AMD64_SHA256" ;; arm64) SUPERCRONIC_SHA256="$SUPERCRONIC_ARM64_SHA256" ;; *) echo "unsupported architecture: $TARGETARCH" >&2; exit 1 ;; esac \
|
|
13
13
|
&& curl -fsSLo /usr/local/bin/supercronic "https://github.com/aptible/supercronic/releases/download/v0.2.46/supercronic-linux-${TARGETARCH}" \
|
|
14
14
|
&& echo "${SUPERCRONIC_SHA256} /usr/local/bin/supercronic" | sha256sum -c - \
|
|
15
15
|
&& chmod 0755 /usr/local/bin/supercronic \
|
|
16
16
|
&& groupadd --gid 10001 ouro \
|
|
17
|
-
&& useradd --uid 10001 --gid 10001 --home-dir /home/ouro --create-home --shell /usr/sbin/nologin ouro
|
|
17
|
+
&& useradd --uid 10001 --gid 10001 --home-dir /home/ouro --create-home --shell /usr/sbin/nologin ouro \
|
|
18
|
+
&& groupadd --system --gid 10002 ouro-sync \
|
|
19
|
+
&& useradd --system --uid 10002 --gid 10002 --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin ouro-sync
|
|
18
20
|
|
|
19
21
|
WORKDIR /opt/ouro
|
|
20
22
|
COPY package.json npm-shrinkwrap.json ./
|
|
@@ -35,6 +37,17 @@ COPY skills ./skills
|
|
|
35
37
|
COPY changelog.json ./changelog.json
|
|
36
38
|
COPY deploy/unraid/container-runtime.json ./container-runtime.json
|
|
37
39
|
COPY deploy/unraid /opt/ouro/deploy/unraid
|
|
40
|
+
# Bundle sync pushes over ssh with the deploy key mounted at /run/ouro-sync. The key is 10002:10002 mode 0400, so
|
|
41
|
+
# only ouro-sync can read it; the agent (ouro, 10001) reaches it only through sudo and the root-owned wrapper, which
|
|
42
|
+
# accepts nothing but git's ssh call for one GitHub repository and trusts only GitHub's published host keys.
|
|
43
|
+
COPY deploy/unraid/github_known_hosts /etc/ouro/github_known_hosts
|
|
44
|
+
COPY deploy/unraid/ouro-sync-ssh /usr/local/libexec/ouro-sync-ssh
|
|
45
|
+
COPY deploy/unraid/ouro-sync.sudoers /etc/sudoers.d/ouro-sync
|
|
46
|
+
RUN chown root:root /etc/ouro /etc/ouro/github_known_hosts /usr/local/libexec/ouro-sync-ssh /etc/sudoers.d/ouro-sync \
|
|
47
|
+
&& chmod 0755 /etc/ouro /usr/local/libexec/ouro-sync-ssh \
|
|
48
|
+
&& chmod 0644 /etc/ouro/github_known_hosts \
|
|
49
|
+
&& chmod 0440 /etc/sudoers.d/ouro-sync \
|
|
50
|
+
&& visudo -cf /etc/sudoers.d/ouro-sync
|
|
38
51
|
COPY assets/sanctuary-host-launcher.sh /opt/ouro/deploy/unraid/sanctuary-host-launcher.sh
|
|
39
52
|
RUN chown -R 10001:10001 /opt/ouro /home/ouro
|
|
40
53
|
|
package/deploy/unraid/README.txt
CHANGED
|
@@ -45,7 +45,34 @@ Effective-spec audit helper:
|
|
|
45
45
|
Before any sequence, define this helper. It captures one actual container
|
|
46
46
|
inspect and its reviewed image inspect without printing either, invokes the
|
|
47
47
|
packaged auditor from the explicit validated runner image argument, and
|
|
48
|
-
removes its mode-0600 inputs on success or shell exit
|
|
48
|
+
removes its mode-0600 inputs on success or shell exit. bundle_sync_enabled
|
|
49
|
+
prints true or false from the bundle's agent.json and fails, with a fixed
|
|
50
|
+
message, when it is a symlink, is not a regular file (a FIFO would block the
|
|
51
|
+
read), or cannot be read or parsed; audits of the container being installed pass `bundle` so the
|
|
52
|
+
auditor requires the sync key mount while sync is on:
|
|
53
|
+
bundle_sync_enabled() {
|
|
54
|
+
BUNDLE_AGENT_JSON=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro/agent.json
|
|
55
|
+
if test -L "$BUNDLE_AGENT_JSON"; then
|
|
56
|
+
printf '%s\n' "bundle sync check: agent.json is a symlink; refusing to read it" >&2
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
/usr/local/bin/node -e '
|
|
60
|
+
const fs = require("node:fs");
|
|
61
|
+
let agent;
|
|
62
|
+
try {
|
|
63
|
+
const fd = fs.openSync(process.argv[1], fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
64
|
+
if (!fs.fstatSync(fd).isFile()) {
|
|
65
|
+
process.stderr.write("bundle sync check: agent.json is not a regular file; refusing to read it\n");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
agent = JSON.parse(fs.readFileSync(fd, "utf8"));
|
|
69
|
+
} catch {
|
|
70
|
+
process.stderr.write("bundle sync check: agent.json is unreadable or not valid JSON\n");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
process.stdout.write(agent?.sync?.enabled === true ? "true" : "false");
|
|
74
|
+
' "$BUNDLE_AGENT_JSON"
|
|
75
|
+
}
|
|
49
76
|
audit_effective() {
|
|
50
77
|
(
|
|
51
78
|
AUDIT_CONTAINER=$1
|
|
@@ -54,6 +81,7 @@ Effective-spec audit helper:
|
|
|
54
81
|
AUDIT_MOUNT_CONTRACT=${4-}
|
|
55
82
|
AUDIT_EXPECTED_IMAGE_REFERENCE=${5-}
|
|
56
83
|
AUDIT_EXPECTED_ICON=${6-}
|
|
84
|
+
AUDIT_SYNC_SOURCE=${7-}
|
|
57
85
|
validate_exact_image_id "$AUDIT_EXPECTED_IMAGE" || return $?
|
|
58
86
|
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
59
87
|
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
@@ -62,6 +90,11 @@ Effective-spec audit helper:
|
|
|
62
90
|
test -n "$AUDIT_EXPECTED_IMAGE_REFERENCE" || return 1
|
|
63
91
|
test "$AUDIT_EXPECTED_ICON" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png || return 1
|
|
64
92
|
set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" --expected-image-reference "$AUDIT_EXPECTED_IMAGE_REFERENCE" --expected-icon "$AUDIT_EXPECTED_ICON"
|
|
93
|
+
case "$AUDIT_SYNC_SOURCE" in
|
|
94
|
+
bundle) AUDIT_SYNC_ENABLED=$(bundle_sync_enabled) || return $?; set -- "$@" --sync-enabled "$AUDIT_SYNC_ENABLED" ;;
|
|
95
|
+
"") ;;
|
|
96
|
+
*) return 1 ;;
|
|
97
|
+
esac
|
|
65
98
|
;;
|
|
66
99
|
legacy-alpha742) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
|
|
67
100
|
*) return 1 ;;
|
|
@@ -920,7 +953,8 @@ Effective-spec audit helper:
|
|
|
920
953
|
validate_exact_image_id "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" || return $?
|
|
921
954
|
printf '%s\n' "$REGISTERED_TEMPLATE_VERSION_IMAGE" | grep -Eq '^ghcr\.io/ourostack/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || return $?
|
|
922
955
|
test -f "$REGISTERED_TEMPLATE_PATH" && test ! -L "$REGISTERED_TEMPLATE_PATH" || return 1
|
|
923
|
-
|
|
956
|
+
REGISTERED_TEMPLATE_SYNC_ENABLED=$(bundle_sync_enabled) || return $?
|
|
957
|
+
docker run --rm --pull=never --network=none --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh --mount "type=bind,src=$REGISTERED_TEMPLATE_PATH,dst=/audit/sanctuary.xml,readonly" "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /opt/ouro/deploy/unraid/container-runtime.json --expected-image-reference "$REGISTERED_TEMPLATE_VERSION_IMAGE" --mount-contract "$REGISTERED_TEMPLATE_MOUNT_CONTRACT" --sync-enabled "$REGISTERED_TEMPLATE_SYNC_ENABLED" || return $?
|
|
924
958
|
}
|
|
925
959
|
verify_known_good_rollback_artifact() {
|
|
926
960
|
EXPECTED_KNOWN_GOOD_IMAGE_ID=$1
|
|
@@ -1203,7 +1237,7 @@ Effective-spec audit helper:
|
|
|
1203
1237
|
test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1204
1238
|
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
1205
1239
|
assert_only_running_butler ouro-butler || return $?
|
|
1206
|
-
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1240
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle || return $?
|
|
1207
1241
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1208
1242
|
chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1209
1243
|
write_dockerman_recovery_evidence committing target-exact-committing "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
@@ -1255,7 +1289,7 @@ Effective-spec audit helper:
|
|
|
1255
1289
|
TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1256
1290
|
test "$TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1257
1291
|
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return 1
|
|
1258
|
-
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1292
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle || return $?
|
|
1259
1293
|
start_only_butler_for_recovery || return $?
|
|
1260
1294
|
wait_butler_ready ouro-butler || return $?
|
|
1261
1295
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
@@ -1265,7 +1299,7 @@ Effective-spec audit helper:
|
|
|
1265
1299
|
enable_butler_autostart || return $?
|
|
1266
1300
|
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null || return $?
|
|
1267
1301
|
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = committing && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"; then
|
|
1268
|
-
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1302
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle || return $?
|
|
1269
1303
|
wait_butler_ready ouro-butler || return $?
|
|
1270
1304
|
verify_butler_autostart "1 0 0 0" || return $?
|
|
1271
1305
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
@@ -2109,12 +2143,13 @@ Update:
|
|
|
2109
2143
|
INSTALLED_SPOOL_SIZE=$(findmnt -bn -o SIZE --target /boot/config/custom/ouro-events/spool)
|
|
2110
2144
|
test "$INSTALLED_SPOOL_SIZE" -le 4194304
|
|
2111
2145
|
}
|
|
2146
|
+
STAGED_SYNC_ENABLED=$(bundle_sync_enabled)
|
|
2112
2147
|
docker run --rm --pull=never --network=none \
|
|
2113
2148
|
--user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
|
|
2114
2149
|
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
2115
2150
|
--mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
|
|
2116
2151
|
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
2117
|
-
"$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE" --mount-contract "$TARGET_MOUNT_CONTRACT"
|
|
2152
|
+
"$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE" --mount-contract "$TARGET_MOUNT_CONTRACT" --sync-enabled "$STAGED_SYNC_ENABLED"
|
|
2118
2153
|
/usr/local/bin/node -e '
|
|
2119
2154
|
const fs = require("node:fs");
|
|
2120
2155
|
const [sourcePath, destinationPath, imageId] = process.argv.slice(1);
|
|
@@ -2134,7 +2169,7 @@ Update:
|
|
|
2134
2169
|
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
2135
2170
|
--mount "type=bind,src=$STAGED_EXACT_TEMPLATE,dst=/audit/sanctuary.exact-image.xml,readonly" \
|
|
2136
2171
|
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
2137
|
-
"$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID" --mount-contract "$TARGET_MOUNT_CONTRACT"
|
|
2172
|
+
"$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID" --mount-contract "$TARGET_MOUNT_CONTRACT" --sync-enabled "$STAGED_SYNC_ENABLED"
|
|
2138
2173
|
recover_dockerman_template_transaction
|
|
2139
2174
|
recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
|
|
2140
2175
|
classify_sanctuary_update_source
|
|
@@ -2301,7 +2336,7 @@ ouro-butler-rollback
|
|
|
2301
2336
|
if test "$(inspect_registry_manifest_digest "$VERSION_IMAGE")" = "$MANIFEST_DIGEST" \
|
|
2302
2337
|
&& test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
|
|
2303
2338
|
&& create_sanctuary_container "$IMAGE_ID" "$VERSION_IMAGE" "$TARGET_MOUNT_CONTRACT" \
|
|
2304
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" \
|
|
2339
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$TARGET_MOUNT_CONTRACT" "$VERSION_IMAGE" "$TEMPLATE_ICON" bundle \
|
|
2305
2340
|
&& assert_only_running_butler - \
|
|
2306
2341
|
&& /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" authority-activate >/dev/null \
|
|
2307
2342
|
&& assert_only_running_butler ouro-butler \
|
|
@@ -2646,7 +2681,7 @@ Restore:
|
|
|
2646
2681
|
&& rm -f "$HOST_RESTORE_INSTALLER" \
|
|
2647
2682
|
&& docker image inspect "$IMAGE_ID" >/dev/null \
|
|
2648
2683
|
&& create_sanctuary_container "$IMAGE_ID" "$RESTORE_VERSION_IMAGE" "$RESTORE_MOUNT_CONTRACT" \
|
|
2649
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_MOUNT_CONTRACT" "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png \
|
|
2684
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_MOUNT_CONTRACT" "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png bundle \
|
|
2650
2685
|
&& assert_only_running_butler - \
|
|
2651
2686
|
&& docker start ouro-butler \
|
|
2652
2687
|
&& assert_only_running_butler ouro-butler \
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
|
|
2
|
+
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
|
|
3
|
+
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Bundle sync's ssh for git. Runs as ouro-sync under sudo, so only ouro-sync can read the deploy key.
|
|
3
|
+
# It accepts only git's own ssh invocation for one GitHub repository and passes nothing else to ssh.
|
|
4
|
+
# It runs no tool through PATH: validation is shell builtins only, and ssh is called by absolute path.
|
|
5
|
+
set -u
|
|
6
|
+
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|
7
|
+
export PATH
|
|
8
|
+
# The character checks below use ranges, which follow the locale under bash.
|
|
9
|
+
LC_ALL=C
|
|
10
|
+
export LC_ALL
|
|
11
|
+
|
|
12
|
+
refuse() {
|
|
13
|
+
printf 'ouro-sync-ssh: refused: %s\n' "$1" >&2
|
|
14
|
+
exit 2
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
login=""
|
|
18
|
+
host=""
|
|
19
|
+
command=""
|
|
20
|
+
have_host=0
|
|
21
|
+
have_command=0
|
|
22
|
+
while [ "$#" -gt 0 ]; do
|
|
23
|
+
case "$1" in
|
|
24
|
+
-o)
|
|
25
|
+
[ "$#" -ge 2 ] && [ "$2" = "SendEnv=GIT_PROTOCOL" ] || refuse "only -o SendEnv=GIT_PROTOCOL is allowed"
|
|
26
|
+
shift 2
|
|
27
|
+
;;
|
|
28
|
+
-p)
|
|
29
|
+
[ "$#" -ge 2 ] && [ "$2" = "22" ] || refuse "only -p 22 is allowed"
|
|
30
|
+
shift 2
|
|
31
|
+
;;
|
|
32
|
+
-l)
|
|
33
|
+
[ "$#" -ge 2 ] && [ "$2" = "git" ] || refuse "only -l git is allowed"
|
|
34
|
+
login=git
|
|
35
|
+
shift 2
|
|
36
|
+
;;
|
|
37
|
+
-*)
|
|
38
|
+
refuse "option $1 is not allowed"
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
if [ "$have_host" -eq 0 ]; then
|
|
42
|
+
host=$1
|
|
43
|
+
have_host=1
|
|
44
|
+
elif [ "$have_command" -eq 0 ]; then
|
|
45
|
+
command=$1
|
|
46
|
+
have_command=1
|
|
47
|
+
else
|
|
48
|
+
refuse "too many arguments"
|
|
49
|
+
fi
|
|
50
|
+
shift
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
case "$host" in
|
|
56
|
+
git@github.com) [ -z "$login" ] || refuse "host must be git@github.com" ;;
|
|
57
|
+
github.com) [ "$login" = git ] || refuse "host must be git@github.com" ;;
|
|
58
|
+
*) refuse "host must be git@github.com" ;;
|
|
59
|
+
esac
|
|
60
|
+
[ "$have_command" -eq 1 ] || refuse "a git command is required"
|
|
61
|
+
case "$command" in
|
|
62
|
+
*"
|
|
63
|
+
"*) refuse "the git command must be one line" ;;
|
|
64
|
+
esac
|
|
65
|
+
bad_command="only git-upload-pack or git-receive-pack for one repository is allowed"
|
|
66
|
+
case "$command" in
|
|
67
|
+
"git-upload-pack "*) quoted=${command#git-upload-pack } ;;
|
|
68
|
+
"git-receive-pack "*) quoted=${command#git-receive-pack } ;;
|
|
69
|
+
*) refuse "$bad_command" ;;
|
|
70
|
+
esac
|
|
71
|
+
case "$quoted" in
|
|
72
|
+
*[!A-Za-z0-9._/\'-]*) refuse "$bad_command" ;;
|
|
73
|
+
esac
|
|
74
|
+
# Either 'owner/repo' with both quotes, or owner/repo with none.
|
|
75
|
+
case "$quoted" in
|
|
76
|
+
\'*\') repo=${quoted#\'}; repo=${repo%\'} ;;
|
|
77
|
+
*\'*) refuse "the repository's quotes must be balanced" ;;
|
|
78
|
+
*) repo=$quoted ;;
|
|
79
|
+
esac
|
|
80
|
+
case "$repo" in
|
|
81
|
+
*/*/*) refuse "$bad_command" ;;
|
|
82
|
+
*/*) ;;
|
|
83
|
+
*) refuse "$bad_command" ;;
|
|
84
|
+
esac
|
|
85
|
+
owner=${repo%%/*}
|
|
86
|
+
name=${repo#*/}
|
|
87
|
+
name=${name%.git}
|
|
88
|
+
for segment in "$owner" "$name"; do
|
|
89
|
+
case "$segment" in
|
|
90
|
+
"" | *[!A-Za-z0-9._-]*) refuse "$bad_command" ;;
|
|
91
|
+
. | ..) refuse "the repository path may not contain . or .." ;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
exec /usr/bin/ssh -i /run/ouro-sync/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes \
|
|
96
|
+
-o UserKnownHostsFile=/etc/ouro/github_known_hosts -o BatchMode=yes \
|
|
97
|
+
-o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 \
|
|
98
|
+
-F /dev/null git@github.com "$command"
|
|
@@ -477,6 +477,11 @@ function enableResidentAutostart() {
|
|
|
477
477
|
// read-only at /etc/ouro/trust/sanctuary. The bundle's recursive chown cannot reach it, and the resident cannot write it.
|
|
478
478
|
export const TRUST_HOST_DIR = "/mnt/user/appdata/ouro-butler/trust/sanctuary"
|
|
479
479
|
export const TRUST_MOUNT = `${TRUST_HOST_DIR}:/etc/ouro/trust/sanctuary:ro`
|
|
480
|
+
// The bundle sync deploy key: root places it on the host, and the Butler reads it through a read-only mount. The key can
|
|
481
|
+
// push to the owner's repo, so nothing inside the Butler can write the mount.
|
|
482
|
+
export const SYNC_HOST_DIR = "/mnt/user/appdata/ouro-butler/sync"
|
|
483
|
+
export const SYNC_TARGET = "/run/ouro-sync"
|
|
484
|
+
export const SYNC_MOUNT = `${SYNC_HOST_DIR}:${SYNC_TARGET}:ro`
|
|
480
485
|
|
|
481
486
|
/** What is wrong with a host trust directory's stat, or null when it is a real root-owned directory closed to group and other writes. */
|
|
482
487
|
export function trustDirIssue(stat) {
|
|
@@ -544,6 +549,7 @@ function recreateResident(version) {
|
|
|
544
549
|
"-v", "/boot/config/custom/ouro-events/spool:/run/ouro-events:ro",
|
|
545
550
|
"-v", "/run/ouro-authority:/run/ouro-authority:ro",
|
|
546
551
|
"-v", TRUST_MOUNT,
|
|
552
|
+
"-v", SYNC_MOUNT,
|
|
547
553
|
image(version)])
|
|
548
554
|
const ref = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}}"]).trim()
|
|
549
555
|
if (ref !== image(version)) fail(`recreated resident is ${ref}, expected ${image(version)}`)
|
|
@@ -1195,6 +1201,23 @@ export function withoutPsycheConfig(text, parse) {
|
|
|
1195
1201
|
return after && psycheConfigs(after) === 0 && after.children.length === before.children.length - 1 ? next : text
|
|
1196
1202
|
}
|
|
1197
1203
|
|
|
1204
|
+
export const SYNC_CONFIG = ` <Config Name="Bundle sync key (read-only)" Target="${SYNC_TARGET}" Default="${SYNC_HOST_DIR}" Mode="ro" Description="Deploy key the Butler's bundle sync pushes with, owned by 10002:10002 mode 0400 so only the sync wrapper reads it. Read-only inside the Butler" Type="Path" Display="always" Required="true" Mask="false">${SYNC_HOST_DIR}</Config>`
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* The DockerMan template with the sync key's read-only Config added before </Container>, so a DockerMan Apply keeps the
|
|
1208
|
+
* mount. `parse` is docker-man-template-xml.cjs's parseDockerManTemplateXml: a template it cannot read, one that already
|
|
1209
|
+
* has a Config for the target, or one where the insertion does not add exactly that element comes back unchanged.
|
|
1210
|
+
*/
|
|
1211
|
+
export function withSyncConfig(text, parse) {
|
|
1212
|
+
const syncConfigs = (document) => document?.children.filter((child) => child.name === "Config" && child.attributes.Target === SYNC_TARGET).length
|
|
1213
|
+
const before = parse(text)
|
|
1214
|
+
if (!before || syncConfigs(before) !== 0) return text
|
|
1215
|
+
const close = text.lastIndexOf("</Container>")
|
|
1216
|
+
const next = `${text.slice(0, close)}${SYNC_CONFIG}\n${text.slice(close)}`
|
|
1217
|
+
const after = parse(next)
|
|
1218
|
+
return after && syncConfigs(after) === 1 && after.children.length === before.children.length + 1 ? next : text
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1198
1221
|
// Keep Unraid's DockerMan template (what the UI's Apply/Update uses) on the live image, without the old psyche mount.
|
|
1199
1222
|
// Runs after the commit, so the parser comes from the installed package (this script is staged on its own, without siblings).
|
|
1200
1223
|
function pinTemplate(version) {
|
|
@@ -1204,9 +1227,10 @@ function pinTemplate(version) {
|
|
|
1204
1227
|
if (!next.includes(`<Repository>${image(version)}</Repository>`)) { console.log(" WARN: DockerMan template Repository not recognised; left unchanged"); return }
|
|
1205
1228
|
try {
|
|
1206
1229
|
const { parseDockerManTemplateXml } = createRequire(import.meta.url)(`${ROOT}/package/deploy/unraid/docker-man-template-xml.cjs`)
|
|
1207
|
-
next = withoutPsycheConfig(next, parseDockerManTemplateXml)
|
|
1208
|
-
} catch (error) { console.log(` WARN: could not load the DockerMan template parser (${error.message}); psyche Config left in place`) }
|
|
1230
|
+
next = withSyncConfig(withoutPsycheConfig(next, parseDockerManTemplateXml), parseDockerManTemplateXml)
|
|
1231
|
+
} catch (error) { console.log(` WARN: could not load the DockerMan template parser (${error.message}); psyche Config left in place, sync key Config not added`) }
|
|
1209
1232
|
if (next.includes(`Target="${PSYCHE_TARGET}"`)) console.log(" WARN: DockerMan template still has a psyche Config; remove it before the next DockerMan Apply")
|
|
1233
|
+
if (!next.includes(`Target="${SYNC_TARGET}"`)) console.log(` WARN: DockerMan template has no ${SYNC_TARGET} Config; add it before the next DockerMan Apply`)
|
|
1210
1234
|
if (next !== text) { writeFileSync(`${TEMPLATE}.prev`, text); writeFileSync(TEMPLATE, next) }
|
|
1211
1235
|
ok(`DockerMan template pins ${image(version)}`)
|
|
1212
1236
|
}
|