@runuai/host 0.9.147 → 0.9.149
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/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +137 -13
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +17 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +125 -58
- package/lib/agents/types.ts +3 -3
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +1681 -138
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/lib/ulid.ts +10 -1
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +941 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2239 -15
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root-side steps that put a task behind the secrets proxy (ADR-153 §3
|
|
3
|
+
* "Redirect" and "Trust inside the task"): the pinned-names block in
|
|
4
|
+
* /etc/hosts, the Uai-owned NAT chain, and the CA in the system store.
|
|
5
|
+
*
|
|
6
|
+
* Each is a POSIX sh script the runner runs as root inside the task —
|
|
7
|
+
* `docker exec --privileged -u root` for the chain on Docker (only that one
|
|
8
|
+
* process gets NET_ADMIN), `container exec -u root` on Apple, the sidecar
|
|
9
|
+
* itself on machines. They are reconciliations, safe to run at every start
|
|
10
|
+
* and on every change: the hosts block is rewritten between its markers,
|
|
11
|
+
* the chain's contents are replaced, and nothing Docker owns is touched.
|
|
12
|
+
*
|
|
13
|
+
* Nothing a task controls is ever interpolated: domains must be canonical
|
|
14
|
+
* and proxiable, addresses must parse, ports and users are checked, and the
|
|
15
|
+
* CA arrives on stdin, never in argv.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { canonicalHostname, isProxiableDomain } from "./hostname";
|
|
19
|
+
|
|
20
|
+
export const HOSTS_BLOCK_BEGIN = "# uai secrets proxy: begin (managed by the runner; do not edit)";
|
|
21
|
+
export const HOSTS_BLOCK_END = "# uai secrets proxy: end";
|
|
22
|
+
/** The task-side loopback address every proxied name resolves to. */
|
|
23
|
+
export const PROXY_PIN_ADDRESS = "127.0.0.2";
|
|
24
|
+
export const PROXY_CHAIN = "UAI_SECRETS";
|
|
25
|
+
/** Where the system store picks up local CAs. Each proxy CA gets its own
|
|
26
|
+
* file there: `openssl rehash` skips a file holding two certificates, and a
|
|
27
|
+
* CA must be a new file name, not new content under an old one, for
|
|
28
|
+
* `update-ca-certificates` to re-hash at all. */
|
|
29
|
+
export const TASK_CA_DIR = "/usr/local/share/ca-certificates";
|
|
30
|
+
/** Every proxy CA the task trusts, in one file: what NODE_EXTRA_CA_CERTS
|
|
31
|
+
* names. Outside TASK_CA_DIR, so the store does not take it twice. */
|
|
32
|
+
export const TASK_CA_BUNDLE_PATH = "/usr/local/share/uai/secrets-proxy-ca.pem";
|
|
33
|
+
|
|
34
|
+
/** The only PATH these root scripts run with, whatever the exec inherited:
|
|
35
|
+
* root-owned system directories only. The inherited one can start with
|
|
36
|
+
* task-writable entries (Apple's asdf shims overlay), and anything found
|
|
37
|
+
* there would run as root. /usr/sbin and /sbin hold Debian's
|
|
38
|
+
* update-ca-certificates and iptables. */
|
|
39
|
+
export const ROOT_SCRIPT_PATH = "/usr/sbin:/usr/bin:/sbin:/bin";
|
|
40
|
+
|
|
41
|
+
/** Sets the PATH (tests pass a directory of fakes ahead of the system one)
|
|
42
|
+
* and clears loader injection for every child the script starts. */
|
|
43
|
+
function rootPrelude(path: string): string {
|
|
44
|
+
return `PATH=${shellQuote(path)}; export PATH; unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shellQuote(value: string): string {
|
|
48
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkedDomains(domains: readonly string[]): string[] {
|
|
52
|
+
const unique = new Set<string>();
|
|
53
|
+
for (const domain of domains) {
|
|
54
|
+
if (canonicalHostname(domain) !== domain || !isProxiableDomain(domain)) {
|
|
55
|
+
throw new Error("refusing to pin a domain that is not canonical and proxiable");
|
|
56
|
+
}
|
|
57
|
+
unique.add(domain);
|
|
58
|
+
}
|
|
59
|
+
return [...unique].sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rewrites the marked block of `hostsPath` so each domain resolves to the
|
|
64
|
+
* pin. With no domains the block is removed. Docker bind-mounts /etc/hosts,
|
|
65
|
+
* where a rename-over fails, so the file is rewritten in place.
|
|
66
|
+
*/
|
|
67
|
+
export function hostsReconcileScript(
|
|
68
|
+
domains: readonly string[],
|
|
69
|
+
hostsPath = "/etc/hosts",
|
|
70
|
+
options: {
|
|
71
|
+
path?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Build the file from this one instead of itself: a task's own view of
|
|
74
|
+
* the hosts on a machine, the machine's file plus the block, which the
|
|
75
|
+
* machine's file never carries. Rebuilt from it each time, so it follows
|
|
76
|
+
* the machine's changes.
|
|
77
|
+
*/
|
|
78
|
+
sourcePath?: string;
|
|
79
|
+
} = {},
|
|
80
|
+
): string {
|
|
81
|
+
// An AAAA lookup of a name the file only pins for IPv4 falls through to DNS
|
|
82
|
+
// and reaches the real upstream. The IPv4-mapped form answers it; a
|
|
83
|
+
// dual-stack socket connects to it over IPv4, into the same chain, and a
|
|
84
|
+
// v6-only socket fails fast rather than going around the proxy.
|
|
85
|
+
const lines = checkedDomains(domains).flatMap((domain) => [
|
|
86
|
+
`${PROXY_PIN_ADDRESS} ${domain}`,
|
|
87
|
+
`::ffff:${PROXY_PIN_ADDRESS} ${domain}`,
|
|
88
|
+
]);
|
|
89
|
+
const block = lines.length === 0 ? [] : [HOSTS_BLOCK_BEGIN, ...lines, HOSTS_BLOCK_END];
|
|
90
|
+
const source = options.sourcePath ?? hostsPath;
|
|
91
|
+
return [
|
|
92
|
+
"set -eu",
|
|
93
|
+
rootPrelude(options.path ?? ROOT_SCRIPT_PATH),
|
|
94
|
+
`hosts=${shellQuote(hostsPath)}`,
|
|
95
|
+
`from=${shellQuote(source)}`,
|
|
96
|
+
'kept=$(mktemp) && next=$(mktemp)',
|
|
97
|
+
'trap \'rm -f "$kept" "$next"\' EXIT',
|
|
98
|
+
// Everything outside a previous block survives, byte for byte.
|
|
99
|
+
`awk -v begin=${shellQuote(HOSTS_BLOCK_BEGIN)} -v end=${shellQuote(HOSTS_BLOCK_END)} ` +
|
|
100
|
+
`'$0 == begin { skip = 1; next } $0 == end { skip = 0; next } !skip { print }' "$from" > "$kept"`,
|
|
101
|
+
`{ cat "$kept"; ${block.map((line) => `printf '%s\\n' ${shellQuote(line)}; `).join("")}} > "$next"`,
|
|
102
|
+
// A view of its own is root's, readable by all; rewritten in place, so
|
|
103
|
+
// a process that has it bind-mounted sees each change.
|
|
104
|
+
...(source === hostsPath ? [] : ['umask 022', 'mkdir -p "$(dirname "$hosts")"']),
|
|
105
|
+
'cat "$next" > "$hosts"',
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Replaces the contents of the Uai-owned NAT chain and makes sure OUTPUT
|
|
111
|
+
* jumps to it exactly once. TCP 443 to the pin goes to the task-local
|
|
112
|
+
* listener; on machines `uidOwner` limits it to the task user's traffic so
|
|
113
|
+
* the root sidecar's own upstream connections are never redirected. With
|
|
114
|
+
* `enabled: false` the chain is emptied (nothing is pinned any more).
|
|
115
|
+
*/
|
|
116
|
+
export function redirectChainScript(options: {
|
|
117
|
+
toPort: number;
|
|
118
|
+
uidOwner?: string;
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
path?: string;
|
|
121
|
+
/**
|
|
122
|
+
* The task's capabilities were fixed when it was created (an Apple VM), so
|
|
123
|
+
* a root exec without NET_ADMIN proves nothing in it ever had it. Never
|
|
124
|
+
* for Docker, where NET_ADMIN comes with each privileged exec: one without
|
|
125
|
+
* it proves nothing about an earlier one.
|
|
126
|
+
*/
|
|
127
|
+
netAdminFromCreation?: boolean;
|
|
128
|
+
/** Where this process's capabilities are read; a test seam. */
|
|
129
|
+
capStatusPath?: string;
|
|
130
|
+
}): string {
|
|
131
|
+
const { toPort, uidOwner, enabled } = options;
|
|
132
|
+
if (!Number.isInteger(toPort) || toPort < 1 || toPort > 65_535) throw new Error("invalid redirect port");
|
|
133
|
+
if (uidOwner !== undefined && !/^[a-z_][a-z0-9_-]{0,31}$/.test(uidOwner)) throw new Error("invalid task user");
|
|
134
|
+
const owner = uidOwner === undefined ? "" : ` -m owner --uid-owner ${uidOwner}`;
|
|
135
|
+
return [
|
|
136
|
+
"set -eu",
|
|
137
|
+
rootPrelude(options.path ?? ROOT_SCRIPT_PATH),
|
|
138
|
+
// An image without iptables cannot have a chain to empty: with nothing to
|
|
139
|
+
// pin there is nothing to do, and a pin needs it.
|
|
140
|
+
"if ! command -v iptables >/dev/null 2>&1 && ! command -v iptables-legacy >/dev/null 2>&1; then",
|
|
141
|
+
` ${enabled ? 'echo "iptables is not installed" >&2; exit 1' : "exit 0"}`,
|
|
142
|
+
"fi",
|
|
143
|
+
// Debian's iptables is the nf_tables backend; a kernel without it still
|
|
144
|
+
// has the legacy tables.
|
|
145
|
+
// Only where the task's capabilities were fixed at its creation: proof
|
|
146
|
+
// that nothing in it can ever have filled the chain, CAP_NET_ADMIN
|
|
147
|
+
// (0x1000) missing from this root exec's effective set. An Apple VM
|
|
148
|
+
// started before it was given the capability never had it: nothing to
|
|
149
|
+
// empty, and no pin possible. An unknown or malformed set proves
|
|
150
|
+
// nothing, and neither does a nat table out of reach: both go on to the
|
|
151
|
+
// tables below, which fail the step, and it is retried.
|
|
152
|
+
...(options.netAdminFromCreation === true
|
|
153
|
+
? [
|
|
154
|
+
`cap_eff=$(awk '$1 == "CapEff:" { print $2 }' ${options.capStatusPath === undefined ? "/proc/$$/status" : shellQuote(options.capStatusPath)} 2>/dev/null || true)`,
|
|
155
|
+
'case "$cap_eff" in',
|
|
156
|
+
" ''|*[!0-9a-fA-F]*) ;;",
|
|
157
|
+
` *) if [ $(( 0x$cap_eff & 0x1000 )) -eq 0 ]; then ${enabled ? 'echo "this task has no NET_ADMIN" >&2; exit 1' : "exit 0"}; fi ;;`,
|
|
158
|
+
"esac",
|
|
159
|
+
]
|
|
160
|
+
: []),
|
|
161
|
+
'ipt=iptables',
|
|
162
|
+
'if ! iptables -t nat -S OUTPUT >/dev/null 2>&1; then ipt=iptables-legacy; fi',
|
|
163
|
+
`"$ipt" -t nat -N ${PROXY_CHAIN} 2>/dev/null || true`,
|
|
164
|
+
`"$ipt" -t nat -F ${PROXY_CHAIN}`,
|
|
165
|
+
...(enabled
|
|
166
|
+
? [
|
|
167
|
+
`"$ipt" -t nat -A ${PROXY_CHAIN} -d ${PROXY_PIN_ADDRESS}/32 -p tcp --dport 443${owner} ` +
|
|
168
|
+
`-j REDIRECT --to-ports ${toPort}`,
|
|
169
|
+
]
|
|
170
|
+
: []),
|
|
171
|
+
`"$ipt" -t nat -C OUTPUT -j ${PROXY_CHAIN} 2>/dev/null || "$ipt" -t nat -I OUTPUT 1 -j ${PROXY_CHAIN}`,
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Installs the CA bundle read from stdin — the signing CA and, inside its
|
|
177
|
+
* last 90 days, its successor — into the task's trust: one
|
|
178
|
+
* `uai-secrets-proxy-<checksum>.crt` per certificate in the system store
|
|
179
|
+
* (stale ones removed) and the whole bundle at the NODE_EXTRA_CA_CERTS path,
|
|
180
|
+
* then rebuilds the store. Anything but one or two distinct PEM certificates
|
|
181
|
+
* is refused before anything is touched; a run with nothing to change exits
|
|
182
|
+
* without rebuilding.
|
|
183
|
+
*/
|
|
184
|
+
export function caInstallScript(
|
|
185
|
+
options: { caDir?: string; bundlePath?: string; systemBundle?: string; path?: string } = {},
|
|
186
|
+
): string {
|
|
187
|
+
const {
|
|
188
|
+
caDir = TASK_CA_DIR,
|
|
189
|
+
bundlePath = TASK_CA_BUNDLE_PATH,
|
|
190
|
+
systemBundle = "/etc/ssl/certs/ca-certificates.crt",
|
|
191
|
+
path = ROOT_SCRIPT_PATH,
|
|
192
|
+
} = options;
|
|
193
|
+
return [
|
|
194
|
+
"set -eu",
|
|
195
|
+
rootPrelude(path),
|
|
196
|
+
"umask 022",
|
|
197
|
+
`dir=${shellQuote(caDir)}`,
|
|
198
|
+
`out=${shellQuote(bundlePath)}`,
|
|
199
|
+
`bundle=${shellQuote(systemBundle)}`,
|
|
200
|
+
// Present from the first change until a rebuild succeeds: a run that
|
|
201
|
+
// removed a retired CA's file and then failed to rebuild leaves a store
|
|
202
|
+
// that still trusts it, which nothing in the files themselves shows.
|
|
203
|
+
'pending="$out.rebuilding"',
|
|
204
|
+
"stage=$(mktemp -d)",
|
|
205
|
+
'trap \'rm -rf "$stage"\' EXIT',
|
|
206
|
+
'cat > "$stage/in"',
|
|
207
|
+
'begins=$(grep -c -- "-----BEGIN CERTIFICATE-----" "$stage/in" || true)',
|
|
208
|
+
'ends=$(grep -c -- "-----END CERTIFICATE-----" "$stage/in" || true)',
|
|
209
|
+
'case "$begins" in 1|2) ;; *) echo "not a CA bundle" >&2; exit 65 ;; esac',
|
|
210
|
+
'[ "$ends" = "$begins" ] || { echo "not a CA bundle" >&2; exit 65; }',
|
|
211
|
+
// Each certificate's first base64 line carries its random serial, so it
|
|
212
|
+
// names that certificate in the system bundle.
|
|
213
|
+
'firsts=$(awk \'prev == "-----BEGIN CERTIFICATE-----" { print } { prev = $0 }\' "$stage/in")',
|
|
214
|
+
'[ "$(printf \'%s\\n\' "$firsts" | grep -c .)" = "$begins" ] || { echo "not a CA bundle" >&2; exit 65; }',
|
|
215
|
+
'[ -z "$(printf \'%s\\n\' "$firsts" | sort | uniq -d)" ] || { echo "not a CA bundle" >&2; exit 65; }',
|
|
216
|
+
// One file per certificate, named by its checksum.
|
|
217
|
+
"awk -v stage=\"$stage\" '/^-----BEGIN CERTIFICATE-----$/ { n++; file = stage \"/cert.\" n } " +
|
|
218
|
+
"file != \"\" { print > file } /^-----END CERTIFICATE-----$/ { close(file); file = \"\" }' \"$stage/in\"",
|
|
219
|
+
'names=""',
|
|
220
|
+
': > "$stage/bundle"',
|
|
221
|
+
'for cert in "$stage"/cert.*; do',
|
|
222
|
+
' name="uai-secrets-proxy-$(cksum < "$cert" | awk \'{ print $1 }\').crt"',
|
|
223
|
+
' mv "$cert" "$stage/$name"',
|
|
224
|
+
' cat "$stage/$name" >> "$stage/bundle"',
|
|
225
|
+
' names="$names $name"',
|
|
226
|
+
"done",
|
|
227
|
+
// Up to date: the same files, no stale ones, the same NODE_EXTRA_CA_CERTS
|
|
228
|
+
// bundle, and every certificate already in the system bundle. A run that
|
|
229
|
+
// placed the files but whose rebuild failed is repaired here.
|
|
230
|
+
"current=1",
|
|
231
|
+
'for name in $names; do cmp -s "$stage/$name" "$dir/$name" || current=0; done',
|
|
232
|
+
'for existing in "$dir"/uai-secrets-proxy*.crt; do',
|
|
233
|
+
' [ -e "$existing" ] || continue',
|
|
234
|
+
' case " $names " in *" ${existing##*/} "*) ;; *) current=0 ;; esac',
|
|
235
|
+
"done",
|
|
236
|
+
'cmp -s "$stage/bundle" "$out" 2>/dev/null || current=0',
|
|
237
|
+
'[ -f "$bundle" ] || current=0',
|
|
238
|
+
'[ ! -e "$pending" ] || current=0',
|
|
239
|
+
'if [ "$current" = 1 ]; then',
|
|
240
|
+
' for first in $firsts; do grep -qxF -- "$first" "$bundle" || current=0; done',
|
|
241
|
+
"fi",
|
|
242
|
+
'[ "$current" = 0 ] || exit 0',
|
|
243
|
+
'mkdir -p "$dir" "$(dirname "$out")"',
|
|
244
|
+
// A rebuild left pending by an earlier run may hide a removal: rebuild
|
|
245
|
+
// everything, as for one.
|
|
246
|
+
"fresh=0",
|
|
247
|
+
'[ ! -e "$pending" ] || fresh=1',
|
|
248
|
+
': > "$pending"',
|
|
249
|
+
'for existing in "$dir"/uai-secrets-proxy*.crt; do',
|
|
250
|
+
' [ -e "$existing" ] || continue',
|
|
251
|
+
' case " $names " in *" ${existing##*/} "*) ;; *) rm -f "$existing"; fresh=1 ;; esac',
|
|
252
|
+
"done",
|
|
253
|
+
// Renamed into place, so no reader ever sees a half-written file.
|
|
254
|
+
'for name in $names; do cp "$stage/$name" "$dir/$name.uai-tmp" && mv "$dir/$name.uai-tmp" "$dir/$name"; done',
|
|
255
|
+
'cp "$stage/bundle" "$out.uai-tmp" && mv "$out.uai-tmp" "$out"',
|
|
256
|
+
// A removal alone adds no link, so the tool would neither re-hash nor
|
|
257
|
+
// clear the retired CA's links; --fresh rebuilds them all (under a
|
|
258
|
+
// second, once per rotation).
|
|
259
|
+
'if [ "$fresh" = 1 ]; then update-ca-certificates --fresh >/dev/null; else update-ca-certificates >/dev/null; fi',
|
|
260
|
+
'rm -f "$pending"',
|
|
261
|
+
].join("\n");
|
|
262
|
+
}
|