@sema-agent/server 7.140.0 → 7.142.0
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/MIGRATION.md +505 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +142 -25
- package/dist/approval-ask-audit-store.d.ts +54 -38
- package/dist/approval-ask-audit-store.js +58 -39
- package/dist/approval-batch-census.d.ts +27 -0
- package/dist/approval-batch-census.js +27 -0
- package/dist/approval-card.d.ts +39 -24
- package/dist/approval-card.js +6 -3
- package/dist/approval-crash-convergence.d.ts +17 -7
- package/dist/approval-crash-convergence.js +94 -22
- package/dist/approval-deny-reasons.d.ts +34 -30
- package/dist/approval-deny-reasons.js +21 -6
- package/dist/approval-policy-settlement.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +33 -1
- package/dist/approval-reconciler.js +43 -40
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +10 -0
- package/dist/audit.d.ts +4 -2
- package/dist/audit.js +2 -2
- package/dist/auth-bridge.d.ts +9 -6
- package/dist/auth-bridge.js +9 -3
- package/dist/boot/adoption.js +2 -1
- package/dist/boot/budget-tracing.d.ts +16 -0
- package/dist/boot/budget-tracing.js +10 -6
- package/dist/boot/device-lane.d.ts +11 -0
- package/dist/boot/device-lane.js +16 -0
- package/dist/boot/engine-lease.d.ts +24 -3
- package/dist/boot/engine-lease.js +9 -2
- package/dist/boot/execution-env.d.ts +24 -4
- package/dist/boot/execution-env.js +34 -27
- package/dist/boot/execution-lane.js +2 -1
- package/dist/boot/exit-boundary.js +5 -4
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/memory-consolidation.js +3 -2
- package/dist/boot/parked-revive-gate.js +2 -1
- package/dist/boot/permission-rules-audit.d.ts +0 -28
- package/dist/boot/permission-rules-audit.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -1
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +4 -3
- package/dist/boot/resolve-spec.js +18 -6
- package/dist/boot/retention-lane.js +5 -3
- package/dist/boot/runner-deps.js +16 -13
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +8 -6
- package/dist/boot/shutdown.js +8 -2
- package/dist/boot/stage-04-stores.js +3 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.js +11 -7
- package/dist/boot/stage-06-runners.d.ts +1 -3
- package/dist/boot/stage-06-runners.js +3 -2
- package/dist/boot/stage-07-capability-layer.d.ts +1 -2
- package/dist/boot/stage-07-capability-layer.js +10 -11
- package/dist/boot/stage-08-reapers.d.ts +1 -2
- package/dist/boot/stage-08-reapers.js +3 -2
- package/dist/boot/stage-09-leader.d.ts +1 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -2
- package/dist/boot/stage-10-http-server.js +6 -4
- package/dist/boot/stores.js +13 -6
- package/dist/boot/web-admin.js +18 -17
- package/dist/boot/webfetch-summarize-lane.js +2 -1
- package/dist/boot/workflow-orchestration.js +3 -2
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/builtin-tools.js +2 -4
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +33 -0
- package/dist/capability-facts.js +32 -0
- package/dist/card-void.d.ts +38 -0
- package/dist/card-void.js +23 -0
- package/dist/config-catalog.js +12 -8
- package/dist/config-provider.d.ts +13 -13
- package/dist/config-provider.js +19 -33
- package/dist/config-types.d.ts +39 -10
- package/dist/config.d.ts +6 -0
- package/dist/config.js +72 -49
- package/dist/delegated-session-owner.d.ts +21 -19
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +3 -0
- package/dist/execution-config-root.d.ts +22 -0
- package/dist/execution-config-root.js +35 -0
- package/dist/execution-lane-caps.d.ts +50 -2
- package/dist/execution-lane-caps.js +90 -13
- package/dist/execution-placement.d.ts +3 -0
- package/dist/execution-placement.js +7 -0
- package/dist/fleet/drain-entry.d.ts +3 -0
- package/dist/fleet/fleet-bus.d.ts +6 -3
- package/dist/hooks/hook-runner.d.ts +6 -0
- package/dist/hooks/hook-runner.js +30 -1
- package/dist/hosted-posture.d.ts +8 -0
- package/dist/hosted-posture.js +4 -0
- package/dist/http/admission.js +3 -3
- package/dist/http/caller-trust.d.ts +3 -2
- package/dist/http/caller-trust.js +2 -1
- package/dist/http/dispatch.js +8 -2
- package/dist/http/execution-context.d.ts +2 -2
- package/dist/http/idempotency.d.ts +1 -1
- package/dist/http/live-frame.d.ts +24 -0
- package/dist/http/live-frame.js +16 -0
- package/dist/http/principal-gate.d.ts +3 -1
- package/dist/http/principal-gate.js +17 -8
- package/dist/http/resume-legs.js +42 -29
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/attachments.js +18 -0
- package/dist/http/routes/capabilities.js +10 -6
- package/dist/http/routes/diagnostics.d.ts +0 -19
- package/dist/http/routes/diagnostics.js +16 -2
- package/dist/http/routes/memory-bundle.d.ts +2 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-policy.js +27 -7
- package/dist/http/routes/run-permission-mode.d.ts +75 -0
- package/dist/http/routes/run-permission-mode.js +170 -0
- package/dist/http/routes/runs.js +3 -3
- package/dist/http/routes/session-memory-sync.d.ts +63 -0
- package/dist/http/routes/session-memory-sync.js +157 -0
- package/dist/http/routes/session-sync.d.ts +3 -6
- package/dist/http/routes/session-sync.js +7 -2
- package/dist/http/routes/sessions-list.d.ts +2 -1
- package/dist/http/routes/sessions.js +5 -4
- package/dist/http/routes/tasks.js +70 -71
- package/dist/http/run-read-face.d.ts +7 -0
- package/dist/http/run-read-face.js +10 -4
- package/dist/http/send.d.ts +6 -1
- package/dist/http/send.js +10 -3
- package/dist/http/server.d.ts +20 -7
- package/dist/http/server.js +2 -4
- package/dist/http/sse-log.d.ts +3 -8
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +3 -1
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +2 -2
- package/dist/http/sync-submit-body.d.ts +21 -4
- package/dist/http/sync-submit-body.js +13 -0
- package/dist/identity-source.d.ts +21 -2
- package/dist/identity-source.js +6 -0
- package/dist/instruction-source-notice.d.ts +7 -0
- package/dist/instruction-source-notice.js +6 -6
- package/dist/mcp-assertion-header.d.ts +4 -0
- package/dist/mcp-assertion-header.js +5 -0
- package/dist/memory-incident-sink.d.ts +19 -6
- package/dist/memory-incident-sink.js +8 -4
- package/dist/memory-posture.d.ts +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +1 -1
- package/dist/memory-sync-client.d.ts +35 -23
- package/dist/memory-sync-client.js +122 -52
- package/dist/memory-sync-identity.d.ts +24 -0
- package/dist/memory-sync-identity.js +57 -0
- package/dist/memory-sync.d.ts +7 -1
- package/dist/memory-sync.js +6 -3
- package/dist/observability/fail-open.d.ts +57 -12
- package/dist/observability/fail-open.js +50 -9
- package/dist/observability/metrics.js +0 -1
- package/dist/observability/run-terminal.d.ts +93 -31
- package/dist/observability/run-terminal.js +175 -171
- package/dist/observability/secret-env-scrub.d.ts +20 -1
- package/dist/observability/secret-env-scrub.js +14 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
- package/dist/orchestration/workflow-completion-inbox.js +13 -5
- package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -1
- package/dist/permission-mode-admission.d.ts +68 -0
- package/dist/permission-mode-admission.js +23 -0
- package/dist/permission-rule-list.d.ts +55 -6
- package/dist/permission-rule-list.js +52 -5
- package/dist/plugins/approval-ask-store-file.d.ts +14 -4
- package/dist/plugins/approval-ask-store-file.js +48 -7
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +9 -8
- package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
- package/dist/plugins/approval-ask-store-sql.js +49 -19
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
- package/dist/plugins/checkpoint-store-sql.js +74 -3
- package/dist/plugins/device-store-sql.d.ts +5 -2
- package/dist/plugins/device-store-sql.js +4 -1
- package/dist/plugins/execution-placement-sql.d.ts +11 -1
- package/dist/plugins/execution-placement-sql.js +12 -1
- package/dist/plugins/file-run-store.d.ts +7 -2
- package/dist/plugins/file-run-store.js +12 -2
- package/dist/plugins/fork-routing-session-store.d.ts +10 -32
- package/dist/plugins/fork-routing-session-store.js +2 -61
- package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
- package/dist/plugins/k8s-exec-protocol.js +6 -1
- package/dist/plugins/local-checkpoint-store.d.ts +10 -1
- package/dist/plugins/local-checkpoint-store.js +22 -0
- package/dist/plugins/local-session-store.d.ts +260 -39
- package/dist/plugins/local-session-store.js +582 -57
- package/dist/plugins/local-session-window.d.ts +17 -3
- package/dist/plugins/local-session-window.js +7 -13
- package/dist/plugins/local-task-attachment-store.d.ts +12 -2
- package/dist/plugins/local-task-attachment-store.js +67 -43
- package/dist/plugins/memory-key-guards.d.ts +3 -3
- package/dist/plugins/memory-run-store.d.ts +7 -2
- package/dist/plugins/memory-run-store.js +12 -2
- package/dist/plugins/one-time-migrations.d.ts +68 -2
- package/dist/plugins/one-time-migrations.js +120 -4
- package/dist/plugins/pg-pool.js +3 -2
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +3 -2
- package/dist/plugins/remote-env-e2b.d.ts +47 -7
- package/dist/plugins/remote-env-e2b.js +242 -46
- package/dist/plugins/remote-env-k8s.d.ts +51 -6
- package/dist/plugins/remote-env-k8s.js +234 -44
- package/dist/plugins/remote-env-local-docker.d.ts +40 -14
- package/dist/plugins/remote-env-local-docker.js +127 -33
- package/dist/plugins/run-store-sql.d.ts +11 -3
- package/dist/plugins/run-store-sql.js +13 -4
- package/dist/plugins/sandbox-read-floor.d.ts +273 -0
- package/dist/plugins/sandbox-read-floor.js +608 -0
- package/dist/plugins/session-ownership.d.ts +2 -1
- package/dist/plugins/session-placement.d.ts +17 -4
- package/dist/plugins/session-placement.js +2 -1
- package/dist/plugins/session-store.d.ts +18 -20
- package/dist/plugins/session-store.js +4 -12
- package/dist/plugins/store-backend.d.ts +41 -7
- package/dist/plugins/store-backend.js +22 -4
- package/dist/plugins/store-contracts.d.ts +35 -3
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +112 -14
- package/dist/plugins/task-attachment-store.js +221 -28
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-session-store.d.ts +4 -1
- package/dist/plugins/tidb-session-store.js +3 -2
- package/dist/plugins/workspace-fill.d.ts +60 -0
- package/dist/plugins/workspace-fill.js +106 -0
- package/dist/project-bundle.d.ts +38 -0
- package/dist/project-bundle.js +609 -0
- package/dist/run-cancel-context.d.ts +34 -0
- package/dist/run-cancel-context.js +7 -0
- package/dist/run-local.js +2 -8
- package/dist/runs.d.ts +39 -12
- package/dist/runs.js +69 -55
- package/dist/sandbox-egress-policy.d.ts +4 -1
- package/dist/sandbox-egress-policy.js +3 -1
- package/dist/sandbox-git-plane.d.ts +31 -12
- package/dist/sandbox-git-plane.js +24 -4
- package/dist/security.d.ts +11 -2
- package/dist/security.js +8 -0
- package/dist/server-secret-env.d.ts +2 -1
- package/dist/server-secret-env.js +2 -4
- package/dist/session-error-wire.d.ts +46 -4
- package/dist/session-error-wire.js +23 -0
- package/dist/session-incarnation.d.ts +9 -3
- package/dist/session-sync-kernel.d.ts +42 -1
- package/dist/session-sync-kernel.js +17 -0
- package/dist/session-sync.js +16 -4
- package/dist/sql-engine-coordinates.d.ts +49 -0
- package/dist/sql-engine-coordinates.js +27 -0
- package/dist/standing-answer-grant.d.ts +31 -2
- package/dist/standing-answer-grant.js +14 -2
- package/dist/store-posture.d.ts +1 -1
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +62 -27
- package/dist/task-settings.js +105 -29
- package/dist/terminal.d.ts +97 -20
- package/dist/terminal.js +107 -18
- package/dist/tool-approval.d.ts +4 -3
- package/dist/tool-approval.js +12 -14
- package/dist/trace/core-keyset-guard.d.ts +196 -2
- package/dist/trace/engine-notice-wire.d.ts +8 -2
- package/dist/trace/engine-notice-wire.js +4 -2
- package/dist/trace/exit-seat.d.ts +9 -1
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +8 -4
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/model-segment.d.ts +2 -2
- package/dist/trace/project.d.ts +18 -1
- package/dist/trace/project.js +16 -0
- package/dist/trace/redact.js +323 -90
- package/dist/trace/self-minted-wire.d.ts +419 -4
- package/dist/trace/self-minted-wire.js +145 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/verifier-pause-card.d.ts +8 -0
- package/dist/verifier-pause-card.js +19 -0
- package/dist/web-admin-client/apply-effective.d.ts +5 -7
- package/dist/web-admin-client/apply-effective.js +22 -2
- package/dist/web-admin-client/candidate-gate.d.ts +1 -1
- package/dist/web-admin-client/effective-keys.js +2 -0
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/types.d.ts +4 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
- package/package.json +2 -2
- package/wire-changes/server@7.141.0.json +367 -0
- package/wire-changes/server@7.142.0.json +639 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { createGunzip, createGzip } from "node:zlib";
|
|
4
|
+
export const PROJECT_BUNDLE_MIME = "application/vnd.sema.project-bundle";
|
|
5
|
+
export function attachmentKindOf(mime) {
|
|
6
|
+
return mime === PROJECT_BUNDLE_MIME ? "project_bundle" : "attachment";
|
|
7
|
+
}
|
|
8
|
+
export const PROJECT_BUNDLE_INVALID_REASONS = ["not_archive", "unsafe_path", "entry_type", "reserved_path", "duplicate_path", "expanded_too_large", "too_many_entries"];
|
|
9
|
+
export const PROJECT_BUNDLE_MAX_EXPANDED_BYTES = 512 * 1024 * 1024;
|
|
10
|
+
export const PROJECT_BUNDLE_MAX_ENTRIES = 200_000;
|
|
11
|
+
export const PROJECT_BUNDLE_RESERVED_TOP_LEVEL = [".git", "attachments"];
|
|
12
|
+
class Refusal extends Error {
|
|
13
|
+
reason;
|
|
14
|
+
detail;
|
|
15
|
+
entryPath;
|
|
16
|
+
constructor(reason, detail, entryPath) {
|
|
17
|
+
super(detail);
|
|
18
|
+
this.reason = reason;
|
|
19
|
+
this.detail = detail;
|
|
20
|
+
this.entryPath = entryPath;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const BLOCK = 512;
|
|
24
|
+
const PATH_MAX_BYTES = 4095;
|
|
25
|
+
const NAME_MAX_BYTES = 255;
|
|
26
|
+
const MAX_LINK_HOPS = 40;
|
|
27
|
+
const YIELD_STEPS = 1 << 16;
|
|
28
|
+
const META_MAX_BYTES = 1024 * 1024;
|
|
29
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
30
|
+
export async function readProjectBundle(bytes, opts = {}) {
|
|
31
|
+
const maxExpanded = opts.limits?.maxExpandedBytes ?? PROJECT_BUNDLE_MAX_EXPANDED_BYTES;
|
|
32
|
+
const maxEntries = opts.limits?.maxEntries ?? PROJECT_BUNDLE_MAX_ENTRIES;
|
|
33
|
+
const writer = opts.canonical === true ? createCanonicalWriter() : undefined;
|
|
34
|
+
const gunzip = createGunzip();
|
|
35
|
+
try {
|
|
36
|
+
const walk = new TarWalk(maxEntries, writer);
|
|
37
|
+
let expanded = 0;
|
|
38
|
+
for await (const chunk of Readable.from([Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)]).pipe(gunzip)) {
|
|
39
|
+
expanded += chunk.length;
|
|
40
|
+
if (expanded > maxExpanded)
|
|
41
|
+
throw new Refusal("expanded_too_large", `the bundle expands past ${maxExpanded} bytes`);
|
|
42
|
+
await walk.feed(chunk);
|
|
43
|
+
if (walk.done)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
const judged = await walk.finish();
|
|
47
|
+
const canonical = writer === undefined ? undefined : await writer.end();
|
|
48
|
+
return { ok: true, entries: judged.entries, topLevel: judged.topLevel, ...(canonical !== undefined ? { canonical } : {}) };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
writer?.abort();
|
|
52
|
+
if (e instanceof Refusal)
|
|
53
|
+
return { ok: false, reason: e.reason, ...(e.entryPath !== undefined ? { path: e.entryPath } : {}), detail: e.detail };
|
|
54
|
+
return { ok: false, reason: "not_archive", detail: `not a gzip-compressed tar: ${e instanceof Error ? e.message : String(e)}` };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
gunzip.destroy();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
class TarWalk {
|
|
61
|
+
writer;
|
|
62
|
+
done = false;
|
|
63
|
+
chunks = [];
|
|
64
|
+
queued = 0;
|
|
65
|
+
entries = 0;
|
|
66
|
+
tree;
|
|
67
|
+
body;
|
|
68
|
+
pax = {};
|
|
69
|
+
unsupportedPax;
|
|
70
|
+
longName;
|
|
71
|
+
longLink;
|
|
72
|
+
zeroBlocks = 0;
|
|
73
|
+
constructor(maxEntries, writer) {
|
|
74
|
+
this.writer = writer;
|
|
75
|
+
this.tree = new BundleTree(maxEntries);
|
|
76
|
+
}
|
|
77
|
+
async feed(chunk) {
|
|
78
|
+
this.chunks.push(chunk);
|
|
79
|
+
this.queued += chunk.length;
|
|
80
|
+
for (;;) {
|
|
81
|
+
if (this.done)
|
|
82
|
+
return;
|
|
83
|
+
if (this.body !== undefined) {
|
|
84
|
+
if (!(await this.drainBody()))
|
|
85
|
+
return;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (this.queued < BLOCK)
|
|
89
|
+
return;
|
|
90
|
+
await this.header(this.take(BLOCK));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async finish() {
|
|
94
|
+
if (!this.done && (this.body !== undefined || this.queued !== 0))
|
|
95
|
+
throw new Refusal("not_archive", "the archive ends inside an entry");
|
|
96
|
+
await this.tree.judge();
|
|
97
|
+
return { entries: this.entries, topLevel: this.tree.topLevel() };
|
|
98
|
+
}
|
|
99
|
+
take(n) {
|
|
100
|
+
const out = Buffer.allocUnsafe(n);
|
|
101
|
+
let off = 0;
|
|
102
|
+
while (off < n) {
|
|
103
|
+
const head = this.chunks[0];
|
|
104
|
+
const k = Math.min(head.length, n - off);
|
|
105
|
+
head.copy(out, off, 0, k);
|
|
106
|
+
off += k;
|
|
107
|
+
if (k === head.length)
|
|
108
|
+
this.chunks.shift();
|
|
109
|
+
else
|
|
110
|
+
this.chunks[0] = head.subarray(k);
|
|
111
|
+
}
|
|
112
|
+
this.queued -= n;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
async drainBody() {
|
|
116
|
+
const b = this.body;
|
|
117
|
+
while (b.remaining > 0) {
|
|
118
|
+
if (this.queued === 0)
|
|
119
|
+
return false;
|
|
120
|
+
const head = this.chunks[0];
|
|
121
|
+
const k = Math.min(head.length, b.remaining);
|
|
122
|
+
const part = head.subarray(0, k);
|
|
123
|
+
if (k === head.length)
|
|
124
|
+
this.chunks.shift();
|
|
125
|
+
else
|
|
126
|
+
this.chunks[0] = head.subarray(k);
|
|
127
|
+
this.queued -= k;
|
|
128
|
+
b.remaining -= k;
|
|
129
|
+
if (b.sink === "write")
|
|
130
|
+
await this.writer.content(part);
|
|
131
|
+
else if (b.sink === "meta")
|
|
132
|
+
b.meta.push(Buffer.from(part));
|
|
133
|
+
}
|
|
134
|
+
if (this.queued < b.pad)
|
|
135
|
+
return false;
|
|
136
|
+
if (b.pad > 0)
|
|
137
|
+
this.take(b.pad);
|
|
138
|
+
this.body = undefined;
|
|
139
|
+
if (b.sink === "write")
|
|
140
|
+
await this.writer.endContent();
|
|
141
|
+
if (b.sink === "meta")
|
|
142
|
+
this.applyMeta(b.metaKind, Buffer.concat(b.meta));
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
async header(h) {
|
|
146
|
+
if (h.every((x) => x === 0)) {
|
|
147
|
+
this.zeroBlocks++;
|
|
148
|
+
if (this.zeroBlocks >= 2)
|
|
149
|
+
this.done = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.zeroBlocks > 0)
|
|
153
|
+
throw new Refusal("not_archive", "a header follows the end-of-archive marker");
|
|
154
|
+
if (!checksumHolds(h))
|
|
155
|
+
throw new Refusal("not_archive", "a tar header's checksum does not hold");
|
|
156
|
+
const magic = h.toString("latin1", 257, 263);
|
|
157
|
+
const posix = magic === "ustar\0";
|
|
158
|
+
if (!posix && magic !== "ustar ")
|
|
159
|
+
throw new Refusal("not_archive", "not a ustar archive (no ustar magic)");
|
|
160
|
+
const type = String.fromCharCode(h[156]);
|
|
161
|
+
const headerSize = octal(h, 124, 12, "size");
|
|
162
|
+
if (type === "x" || type === "g" || type === "L" || type === "K") {
|
|
163
|
+
if (headerSize > META_MAX_BYTES)
|
|
164
|
+
throw new Refusal("not_archive", `an extended header of ${headerSize} bytes is past the ${META_MAX_BYTES}-byte bound`);
|
|
165
|
+
this.body = { remaining: headerSize, pad: padOf(headerSize), sink: "meta", meta: [], metaKind: type };
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const size = this.pax.size ?? headerSize;
|
|
169
|
+
const rawName = this.pax.path ?? this.longName ?? joinUstarName(posix ? cstr(h, 345, 155) : "", cstr(h, 0, 100));
|
|
170
|
+
const rawLink = this.pax.linkpath ?? this.longLink ?? cstr(h, 157, 100);
|
|
171
|
+
const unsupported = this.unsupportedPax;
|
|
172
|
+
this.pax = {};
|
|
173
|
+
this.unsupportedPax = undefined;
|
|
174
|
+
this.longName = undefined;
|
|
175
|
+
this.longLink = undefined;
|
|
176
|
+
const entryType = type === "0" || type === "\0" ? "file" : type === "5" ? "dir" : type === "2" ? "symlink" : undefined;
|
|
177
|
+
const name = decodeName(rawName);
|
|
178
|
+
if (entryType === undefined)
|
|
179
|
+
throw new Refusal("entry_type", `'${name}' is a ${tarTypeWord(type)} — a project bundle holds regular files, directories and symbolic links only`, name);
|
|
180
|
+
if (unsupported !== undefined) {
|
|
181
|
+
throw new Refusal("entry_type", `'${name}' carries the pax extension '${printable(unsupported)}' — a project bundle holds plain regular files, directories and symbolic links only (a sparse map, a volume offset or an unknown extension changes how the entry reads)`, name);
|
|
182
|
+
}
|
|
183
|
+
const p = landingOf(name);
|
|
184
|
+
if (p === "") {
|
|
185
|
+
if (entryType !== "dir")
|
|
186
|
+
throw new Refusal("unsafe_path", `an entry with an empty path is not a ${entryType}`, name);
|
|
187
|
+
this.body = { remaining: size, pad: padOf(size), sink: "skip" };
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const slash = p.indexOf("/");
|
|
191
|
+
if (PROJECT_BUNDLE_RESERVED_TOP_LEVEL.includes(slash === -1 ? p : p.slice(0, slash)))
|
|
192
|
+
throw new Refusal("reserved_path", `'${p}' is under a reserved top-level name (${PROJECT_BUNDLE_RESERVED_TOP_LEVEL.join(", ")})`, p);
|
|
193
|
+
let linkname = "";
|
|
194
|
+
if (entryType === "symlink") {
|
|
195
|
+
linkname = decodeName(rawLink);
|
|
196
|
+
if (linkname === "" || linkname.startsWith("/") || /[\x00-\x1f\x7f]/.test(linkname) || Buffer.byteLength(linkname) > PATH_MAX_BYTES) {
|
|
197
|
+
throw new Refusal("unsafe_path", `the symbolic link '${p}' needs a non-empty relative target of at most ${PATH_MAX_BYTES} bytes without control characters`, p);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this.tree.place(p, entryType, linkname);
|
|
201
|
+
this.entries++;
|
|
202
|
+
const entry = { path: p, type: entryType, mode: octal(h, 100, 8, "mode"), mtime: octalOrZero(h, 136, 12), size: entryType === "file" ? size : 0, linkname };
|
|
203
|
+
if (this.writer !== undefined)
|
|
204
|
+
await this.writer.entry(entry);
|
|
205
|
+
this.body = { remaining: size, pad: padOf(size), sink: entryType === "file" && this.writer !== undefined ? "write" : "skip" };
|
|
206
|
+
}
|
|
207
|
+
applyMeta(kind, data) {
|
|
208
|
+
if (kind === "L")
|
|
209
|
+
return void (this.longName = data.toString("latin1").replace(/\0+$/, ""));
|
|
210
|
+
if (kind === "K")
|
|
211
|
+
return void (this.longLink = data.toString("latin1").replace(/\0+$/, ""));
|
|
212
|
+
const records = parsePax(data);
|
|
213
|
+
for (const key of records.keys()) {
|
|
214
|
+
if (paxKeyReading(key) !== "unsupported")
|
|
215
|
+
continue;
|
|
216
|
+
if (kind === "g")
|
|
217
|
+
throw new Refusal("entry_type", `a global pax header carries the extension '${printable(key)}' — a project bundle holds plain regular files, directories and symbolic links only`);
|
|
218
|
+
this.unsupportedPax ??= key;
|
|
219
|
+
}
|
|
220
|
+
if (kind === "g") {
|
|
221
|
+
if (records.has("path") || records.has("linkpath") || records.has("size"))
|
|
222
|
+
throw new Refusal("not_archive", "a global pax header sets path / linkpath / size");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const sizeText = records.get("size");
|
|
226
|
+
if (sizeText !== undefined && !/^\d{1,12}$/.test(sizeText))
|
|
227
|
+
throw new Refusal("not_archive", "a pax size record is not a decimal byte count");
|
|
228
|
+
this.pax = {
|
|
229
|
+
...(records.has("path") ? { path: records.get("path") } : {}),
|
|
230
|
+
...(records.has("linkpath") ? { linkpath: records.get("linkpath") } : {}),
|
|
231
|
+
...(sizeText !== undefined ? { size: Number(sizeText) } : {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function padOf(size) {
|
|
236
|
+
return (BLOCK - (size % BLOCK)) % BLOCK;
|
|
237
|
+
}
|
|
238
|
+
function checksumHolds(h) {
|
|
239
|
+
const stored = octalOrNaN(h, 148, 8);
|
|
240
|
+
let unsigned = 0;
|
|
241
|
+
let signed = 0;
|
|
242
|
+
for (let i = 0; i < BLOCK; i++) {
|
|
243
|
+
const b = i >= 148 && i < 156 ? 0x20 : h[i];
|
|
244
|
+
unsigned += b;
|
|
245
|
+
signed += b > 127 ? b - 256 : b;
|
|
246
|
+
}
|
|
247
|
+
return stored === unsigned || stored === signed;
|
|
248
|
+
}
|
|
249
|
+
function octalOrNaN(h, off, len) {
|
|
250
|
+
const s = h.toString("latin1", off, off + len).replace(/[\0 ]+$/, "").replace(/^ +/, "");
|
|
251
|
+
return /^[0-7]+$/.test(s) ? parseInt(s, 8) : Number.NaN;
|
|
252
|
+
}
|
|
253
|
+
function octal(h, off, len, field) {
|
|
254
|
+
if (h[off] & 0x80)
|
|
255
|
+
throw new Refusal("not_archive", `the ${field} field is in base-256 form`);
|
|
256
|
+
const s = h.toString("latin1", off, off + len).replace(/[\0 ]+$/, "").replace(/^ +/, "");
|
|
257
|
+
if (s === "")
|
|
258
|
+
return 0;
|
|
259
|
+
if (!/^[0-7]+$/.test(s))
|
|
260
|
+
throw new Refusal("not_archive", `the ${field} field is not octal`);
|
|
261
|
+
return parseInt(s, 8);
|
|
262
|
+
}
|
|
263
|
+
function octalOrZero(h, off, len) {
|
|
264
|
+
const v = octalOrNaN(h, off, len);
|
|
265
|
+
return Number.isFinite(v) && v >= 0 ? v : 0;
|
|
266
|
+
}
|
|
267
|
+
function cstr(h, off, len) {
|
|
268
|
+
const end = h.indexOf(0, off);
|
|
269
|
+
return h.toString("latin1", off, end === -1 || end > off + len ? off + len : end);
|
|
270
|
+
}
|
|
271
|
+
function joinUstarName(prefix, name) {
|
|
272
|
+
return prefix === "" ? name : `${prefix}/${name}`;
|
|
273
|
+
}
|
|
274
|
+
function decodeName(raw) {
|
|
275
|
+
try {
|
|
276
|
+
return UTF8.decode(Buffer.from(raw, "latin1"));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
throw new Refusal("unsafe_path", "an entry path is not valid UTF-8", raw.replace(/[^\x20-\x7e]/g, "?"));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function parsePax(data) {
|
|
283
|
+
const out = new Map();
|
|
284
|
+
let off = 0;
|
|
285
|
+
while (off < data.length) {
|
|
286
|
+
const sp = data.indexOf(0x20, off);
|
|
287
|
+
if (sp === -1)
|
|
288
|
+
throw new Refusal("not_archive", "a pax record has no length");
|
|
289
|
+
const lenText = data.toString("latin1", off, sp);
|
|
290
|
+
if (!/^\d{1,7}$/.test(lenText))
|
|
291
|
+
throw new Refusal("not_archive", "a pax record length is not decimal");
|
|
292
|
+
const len = Number(lenText);
|
|
293
|
+
if (len <= sp - off + 1 || off + len > data.length || data[off + len - 1] !== 0x0a)
|
|
294
|
+
throw new Refusal("not_archive", "a pax record is malformed");
|
|
295
|
+
const rec = data.toString("latin1", sp + 1, off + len - 1);
|
|
296
|
+
const eq = rec.indexOf("=");
|
|
297
|
+
if (eq <= 0)
|
|
298
|
+
throw new Refusal("not_archive", "a pax record has no key");
|
|
299
|
+
out.set(rec.slice(0, eq), rec.slice(eq + 1));
|
|
300
|
+
off += len;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
function tarTypeWord(type) {
|
|
305
|
+
switch (type) {
|
|
306
|
+
case "1":
|
|
307
|
+
return "hard link";
|
|
308
|
+
case "3":
|
|
309
|
+
return "character device";
|
|
310
|
+
case "4":
|
|
311
|
+
return "block device";
|
|
312
|
+
case "6":
|
|
313
|
+
return "FIFO";
|
|
314
|
+
case "7":
|
|
315
|
+
return "contiguous file";
|
|
316
|
+
case "S":
|
|
317
|
+
return "sparse file";
|
|
318
|
+
default:
|
|
319
|
+
return `tar entry of type '${type.replace(/[^\x20-\x7e]/g, "?")}'`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function landingOf(name) {
|
|
323
|
+
if (/[\x00-\x1f\x7f]/.test(name))
|
|
324
|
+
throw new Refusal("unsafe_path", `an entry path carries a control character`, name.replace(/[\x00-\x1f\x7f]/g, "?"));
|
|
325
|
+
if (name.startsWith("/"))
|
|
326
|
+
throw new Refusal("unsafe_path", `'${name}' is an absolute path`, name);
|
|
327
|
+
const segs = [];
|
|
328
|
+
for (const seg of name.split("/")) {
|
|
329
|
+
if (seg === "" || seg === ".")
|
|
330
|
+
continue;
|
|
331
|
+
if (seg === "..")
|
|
332
|
+
throw new Refusal("unsafe_path", `'${name}' has a '..' segment`, name);
|
|
333
|
+
segs.push(seg);
|
|
334
|
+
}
|
|
335
|
+
const p = segs.join("/");
|
|
336
|
+
if (Buffer.byteLength(p) > PATH_MAX_BYTES)
|
|
337
|
+
throw tooLong(p);
|
|
338
|
+
const wide = segs.find((seg) => Buffer.byteLength(seg) > NAME_MAX_BYTES);
|
|
339
|
+
if (wide !== undefined)
|
|
340
|
+
throw new Refusal("unsafe_path", `'${p}' has a name of ${Buffer.byteLength(wide)} bytes — the sandbox's filesystem holds at most ${NAME_MAX_BYTES} bytes per name`, p);
|
|
341
|
+
return p;
|
|
342
|
+
}
|
|
343
|
+
function tooLong(p) {
|
|
344
|
+
return new Refusal("unsafe_path", `an entry path of ${Buffer.byteLength(p)} bytes (starting '${printable(p.slice(0, 64))}…') — the sandbox's filesystem holds at most ${PATH_MAX_BYTES} bytes per path`);
|
|
345
|
+
}
|
|
346
|
+
function printable(s) {
|
|
347
|
+
return s.replace(/[^\x20-\x7e]/g, "?");
|
|
348
|
+
}
|
|
349
|
+
function paxKeyReading(key) {
|
|
350
|
+
switch (key) {
|
|
351
|
+
case "path":
|
|
352
|
+
case "linkpath":
|
|
353
|
+
case "size":
|
|
354
|
+
return key;
|
|
355
|
+
case "mtime":
|
|
356
|
+
case "atime":
|
|
357
|
+
case "ctime":
|
|
358
|
+
case "uid":
|
|
359
|
+
case "gid":
|
|
360
|
+
case "uname":
|
|
361
|
+
case "gname":
|
|
362
|
+
case "comment":
|
|
363
|
+
case "charset":
|
|
364
|
+
case "hdrcharset":
|
|
365
|
+
case "SCHILY.fflags":
|
|
366
|
+
case "SCHILY.dev":
|
|
367
|
+
case "SCHILY.ino":
|
|
368
|
+
case "SCHILY.nlink":
|
|
369
|
+
case "LIBARCHIVE.creationtime":
|
|
370
|
+
case "LIBARCHIVE.symlinktype":
|
|
371
|
+
return "metadata";
|
|
372
|
+
default:
|
|
373
|
+
return PAX_METADATA_FAMILIES.some((family) => key.startsWith(family)) ? "metadata" : "unsupported";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const PAX_METADATA_FAMILIES = ["SCHILY.xattr.", "LIBARCHIVE.xattr.", "SCHILY.acl.", "security.", "RHT.security."];
|
|
377
|
+
class BundleTree {
|
|
378
|
+
maxObjects;
|
|
379
|
+
root = { path: "", parent: undefined, kind: "dir", children: new Map(), linkname: "", firstUnder: "" };
|
|
380
|
+
byPath = new Map();
|
|
381
|
+
links = [];
|
|
382
|
+
objects = 0;
|
|
383
|
+
offense;
|
|
384
|
+
steps = 0;
|
|
385
|
+
constructor(maxObjects) {
|
|
386
|
+
this.maxObjects = maxObjects;
|
|
387
|
+
}
|
|
388
|
+
place(p, type, linkname) {
|
|
389
|
+
const cut = p.lastIndexOf("/");
|
|
390
|
+
const parent = cut === -1 ? this.root : this.dirAt(p.slice(0, cut), p);
|
|
391
|
+
if (parent === undefined)
|
|
392
|
+
return;
|
|
393
|
+
if (parent.kind === "file" || parent.kind === "symlink")
|
|
394
|
+
return this.offend(p, parent);
|
|
395
|
+
const name = cut === -1 ? p : p.slice(cut + 1);
|
|
396
|
+
const known = parent.children.get(name);
|
|
397
|
+
if (known === undefined) {
|
|
398
|
+
this.add(parent, name, p, type, linkname, p);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (known.kind !== "implied")
|
|
402
|
+
throw new Refusal("duplicate_path", `'${p}' appears twice`, p);
|
|
403
|
+
known.kind = type;
|
|
404
|
+
known.linkname = linkname;
|
|
405
|
+
if (type === "symlink")
|
|
406
|
+
this.links.push(known);
|
|
407
|
+
if (type !== "dir")
|
|
408
|
+
this.offend(known.firstUnder, known);
|
|
409
|
+
}
|
|
410
|
+
dirAt(dirPath, entryPath) {
|
|
411
|
+
const known = this.byPath.get(dirPath);
|
|
412
|
+
if (known !== undefined)
|
|
413
|
+
return known;
|
|
414
|
+
const ends = [];
|
|
415
|
+
let base = this.root;
|
|
416
|
+
for (let end = dirPath.length;;) {
|
|
417
|
+
ends.push(end);
|
|
418
|
+
const up = dirPath.lastIndexOf("/", end - 1);
|
|
419
|
+
if (up === -1)
|
|
420
|
+
break;
|
|
421
|
+
const found = this.byPath.get(dirPath.slice(0, up));
|
|
422
|
+
if (found !== undefined) {
|
|
423
|
+
base = found;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
end = up;
|
|
427
|
+
}
|
|
428
|
+
let cur = base;
|
|
429
|
+
for (let k = ends.length - 1; k >= 0; k--) {
|
|
430
|
+
if (cur.kind === "file" || cur.kind === "symlink") {
|
|
431
|
+
this.offend(entryPath, cur);
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
const end = ends[k];
|
|
435
|
+
cur = this.add(cur, dirPath.slice(dirPath.lastIndexOf("/", end - 1) + 1, end), dirPath.slice(0, end), "implied", "", entryPath);
|
|
436
|
+
}
|
|
437
|
+
return cur;
|
|
438
|
+
}
|
|
439
|
+
add(parent, name, p, kind, linkname, firstUnder) {
|
|
440
|
+
if (this.objects >= this.maxObjects)
|
|
441
|
+
throw new Refusal("too_many_entries", `the bundle unpacks to more than ${this.maxObjects} filesystem objects (its entries and the directories their paths imply)`);
|
|
442
|
+
this.objects++;
|
|
443
|
+
const node = { path: p, parent, kind, children: new Map(), linkname, firstUnder };
|
|
444
|
+
parent.children.set(name, node);
|
|
445
|
+
this.byPath.set(p, node);
|
|
446
|
+
if (kind === "symlink")
|
|
447
|
+
this.links.push(node);
|
|
448
|
+
return node;
|
|
449
|
+
}
|
|
450
|
+
offend(entryPath, blocker) {
|
|
451
|
+
this.offense ??= { entryPath, blocker };
|
|
452
|
+
}
|
|
453
|
+
topLevel() {
|
|
454
|
+
return [...this.root.children.keys()].sort();
|
|
455
|
+
}
|
|
456
|
+
async judge() {
|
|
457
|
+
for (const link of this.links) {
|
|
458
|
+
this.landingOf(link, 0);
|
|
459
|
+
if (this.steps >= YIELD_STEPS) {
|
|
460
|
+
this.steps = 0;
|
|
461
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (this.offense !== undefined)
|
|
465
|
+
throw this.offenseRefusal(this.offense);
|
|
466
|
+
}
|
|
467
|
+
offenseRefusal({ entryPath, blocker }) {
|
|
468
|
+
if (blocker.kind === "file")
|
|
469
|
+
return new Refusal("unsafe_path", `'${entryPath}' is under the regular file '${blocker.path}'`, entryPath);
|
|
470
|
+
const end = this.walk(this.landingOf(blocker, 0), entryPath.slice(blocker.path.length + 1), 0, blocker);
|
|
471
|
+
const top = end.node === this.root && end.depth > 0 ? end.top : undefined;
|
|
472
|
+
if (top !== undefined && PROJECT_BUNDLE_RESERVED_TOP_LEVEL.includes(top)) {
|
|
473
|
+
return new Refusal("reserved_path", `'${entryPath}' lands under '${top}' through the symbolic link '${blocker.path}' of the bundle — a reserved top-level name`, entryPath);
|
|
474
|
+
}
|
|
475
|
+
return new Refusal("unsafe_path", `'${entryPath}' is written through a symbolic link of the bundle ('${blocker.path}')`, entryPath);
|
|
476
|
+
}
|
|
477
|
+
landingOf(link, hops) {
|
|
478
|
+
if (link.landing !== undefined)
|
|
479
|
+
return link.landing;
|
|
480
|
+
if (hops >= MAX_LINK_HOPS)
|
|
481
|
+
throw new Refusal("unsafe_path", `the symbolic link '${link.path}' -> '${link.linkname}' does not land within ${MAX_LINK_HOPS} links (the bundle's links form a loop, or a chain that deep)`, link.path);
|
|
482
|
+
const at = this.walk({ node: link.parent, depth: 0, top: undefined }, link.linkname, hops + 1, link);
|
|
483
|
+
link.landing = at;
|
|
484
|
+
return at;
|
|
485
|
+
}
|
|
486
|
+
walk(from, text, hops, owner) {
|
|
487
|
+
let { node, depth, top } = from;
|
|
488
|
+
for (let i = 0; i <= text.length;) {
|
|
489
|
+
let j = text.indexOf("/", i);
|
|
490
|
+
if (j === -1)
|
|
491
|
+
j = text.length;
|
|
492
|
+
const seg = text.slice(i, j);
|
|
493
|
+
i = j + 1;
|
|
494
|
+
this.steps++;
|
|
495
|
+
if (seg === "" || seg === ".")
|
|
496
|
+
continue;
|
|
497
|
+
if (seg === "..") {
|
|
498
|
+
if (depth > 0) {
|
|
499
|
+
depth--;
|
|
500
|
+
if (depth === 0)
|
|
501
|
+
top = undefined;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (node.parent === undefined)
|
|
505
|
+
throw new Refusal("unsafe_path", `the symbolic link '${owner.path}' -> '${owner.linkname}' does not stay inside the bundle (resolved through the bundle's own links)`, owner.path);
|
|
506
|
+
node = node.parent;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (depth > 0) {
|
|
510
|
+
depth++;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const child = node.children.get(seg);
|
|
514
|
+
if (child === undefined) {
|
|
515
|
+
if (node === this.root)
|
|
516
|
+
top = seg;
|
|
517
|
+
depth = 1;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (child.kind === "symlink") {
|
|
521
|
+
({ node, depth, top } = this.landingOf(child, hops));
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
node = child;
|
|
525
|
+
}
|
|
526
|
+
return { node, depth, top };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function createCanonicalWriter() {
|
|
530
|
+
const gz = createGzip();
|
|
531
|
+
const out = [];
|
|
532
|
+
gz.on("data", (b) => out.push(b));
|
|
533
|
+
let pending = 0;
|
|
534
|
+
const write = async (b) => {
|
|
535
|
+
if (!gz.write(b))
|
|
536
|
+
await once(gz, "drain");
|
|
537
|
+
};
|
|
538
|
+
return {
|
|
539
|
+
async entry(e) {
|
|
540
|
+
const name = e.type === "dir" ? `${e.path}/` : e.path;
|
|
541
|
+
const needsPax = Buffer.byteLength(name) > 100 || Buffer.byteLength(e.linkname) > 100;
|
|
542
|
+
if (needsPax) {
|
|
543
|
+
const recs = Buffer.from(paxRecord("path", name) + (Buffer.byteLength(e.linkname) > 100 ? paxRecord("linkpath", e.linkname) : ""), "utf8");
|
|
544
|
+
await write(ustarHeader({ name: "PaxHeader", type: "x", mode: 0o644, size: recs.length, mtime: e.mtime, linkname: "" }));
|
|
545
|
+
await write(recs);
|
|
546
|
+
await write(Buffer.alloc(padOf(recs.length)));
|
|
547
|
+
}
|
|
548
|
+
const mode = e.type === "file" ? (e.mode & 0o111 ? 0o755 : 0o644) : e.type === "dir" ? 0o755 : 0o777;
|
|
549
|
+
const type = e.type === "file" ? "0" : e.type === "dir" ? "5" : "2";
|
|
550
|
+
await write(ustarHeader({ name: needsPax ? truncateUtf8(name, 100) : name, type, mode, size: e.size, mtime: e.mtime, linkname: needsPax ? truncateUtf8(e.linkname, 100) : e.linkname }));
|
|
551
|
+
pending = e.size;
|
|
552
|
+
},
|
|
553
|
+
async content(part) {
|
|
554
|
+
await write(part);
|
|
555
|
+
},
|
|
556
|
+
async endContent() {
|
|
557
|
+
await write(Buffer.alloc(padOf(pending)));
|
|
558
|
+
pending = 0;
|
|
559
|
+
},
|
|
560
|
+
async end() {
|
|
561
|
+
await write(Buffer.alloc(BLOCK * 2));
|
|
562
|
+
gz.end();
|
|
563
|
+
await once(gz, "end");
|
|
564
|
+
return new Uint8Array(Buffer.concat(out));
|
|
565
|
+
},
|
|
566
|
+
abort() {
|
|
567
|
+
gz.destroy();
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function paxRecord(key, value) {
|
|
572
|
+
const body = ` ${key}=${value}\n`;
|
|
573
|
+
let len = Buffer.byteLength(body) + 1;
|
|
574
|
+
while (String(len).length + Buffer.byteLength(body) !== len)
|
|
575
|
+
len = String(len).length + Buffer.byteLength(body);
|
|
576
|
+
return `${len}${body}`;
|
|
577
|
+
}
|
|
578
|
+
function truncateUtf8(s, max) {
|
|
579
|
+
let b = Buffer.from(s, "utf8");
|
|
580
|
+
if (b.length <= max)
|
|
581
|
+
return s;
|
|
582
|
+
b = b.subarray(0, max);
|
|
583
|
+
return b.toString("utf8").replace(/�+$/, "");
|
|
584
|
+
}
|
|
585
|
+
function ustarHeader(f) {
|
|
586
|
+
const h = Buffer.alloc(BLOCK, 0);
|
|
587
|
+
Buffer.from(f.name, "utf8").copy(h, 0, 0, 100);
|
|
588
|
+
h.write(octalField(f.mode, 8), 100, "latin1");
|
|
589
|
+
h.write(octalField(0, 8), 108, "latin1");
|
|
590
|
+
h.write(octalField(0, 8), 116, "latin1");
|
|
591
|
+
h.write(octalField(f.size, 12), 124, "latin1");
|
|
592
|
+
h.write(octalField(Math.min(f.mtime, 0o77777777777), 12), 136, "latin1");
|
|
593
|
+
h.write(" ", 148, "latin1");
|
|
594
|
+
h.write(f.type, 156, "latin1");
|
|
595
|
+
Buffer.from(f.linkname, "utf8").copy(h, 157, 0, 100);
|
|
596
|
+
h.write("ustar\0", 257, "latin1");
|
|
597
|
+
h.write("00", 263, "latin1");
|
|
598
|
+
h.write(octalField(0, 8), 329, "latin1");
|
|
599
|
+
h.write(octalField(0, 8), 337, "latin1");
|
|
600
|
+
let sum = 0;
|
|
601
|
+
for (let i = 0; i < BLOCK; i++)
|
|
602
|
+
sum += h[i];
|
|
603
|
+
h.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, "latin1");
|
|
604
|
+
return h;
|
|
605
|
+
}
|
|
606
|
+
function octalField(n, width) {
|
|
607
|
+
return `${Math.max(0, Math.floor(n)).toString(8).padStart(width - 1, "0")}\0`;
|
|
608
|
+
}
|
|
609
|
+
//# sourceMappingURL=project-bundle.js.map
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [ref] 件3(cli [ref] sema-bug4 (d) / server [ref]):**取消现场快照**的唯一铸点。
|
|
3
|
+
*
|
|
4
|
+
* 语义边界、缺席纪律、「为什么不写模型调用相位」全部成文在类型属主处 —— 见
|
|
5
|
+
* `plugins/store-contracts.ts` 的 `RunCancelContext` 顶注。本文件只做装配。
|
|
6
|
+
*
|
|
7
|
+
* 三条用户取消腿共用它(**判别材料只许有一份口径**):
|
|
8
|
+
* · `POST /v1/runs/:id/cancel` 的 park 取消腿(`http/routes/runs.ts` cancelSuspended);
|
|
9
|
+
* · `/v1/tasks/stream` 的客户端断连腿(`http/routes/tasks.ts`,没读到 done 的那一支);
|
|
10
|
+
* · durable bg 腿的 abort 结算(`runs.ts` —— 同一条 cancel 动词打到 running 行上时的终局形)。
|
|
11
|
+
*
|
|
12
|
+
* [ref] 起,「取消终局整只长什么样」也收在本文件({@link mintCancelledResult})—— 快照与它所在的
|
|
13
|
+
* 那条结果由同一个属主铸,三条腿一份口径。
|
|
14
|
+
*
|
|
15
|
+
* [ref] 起,「这次取消是谁发起的」也收在本文件:中止那一刻经 {@link createRunAbort} 的句柄登记成因,终局铸点从同一只句柄读。
|
|
16
|
+
*/
|
|
17
|
+
import type { TerminalCause } from "@sema-agent/core";
|
|
1
18
|
import type { AbortCause, PersistedTaskResult, RunCancelContext } from "./plugins/store-contracts.js";
|
|
2
19
|
/**
|
|
3
20
|
* @param taskId 这条 run 的账本 id(turn 活性登记的键)。
|
|
@@ -58,6 +75,23 @@ export declare function mintCancelledResult<R extends PersistedTaskResult>(args:
|
|
|
58
75
|
export declare function mintCancelledResult(args: CancelMintArgs & {
|
|
59
76
|
engineResult?: undefined;
|
|
60
77
|
}): PersistedTaskResult;
|
|
78
|
+
/**
|
|
79
|
+
* 被取消的续跑腿、且这张卡被本服务 expire 之后,终局上的那句话 —— **全仓单源**([ref])。
|
|
80
|
+
*
|
|
81
|
+
* 引擎对一条「批过的那一步没开跑就结束」的续跑腿,在终局文案上补一句通用的话:「…the checkpoint was REOPENED (pending): the same approval can be resumed again」
|
|
82
|
+
* (它对想再续的宿主是真话:引擎确实把卡放回了 pending)。而本服务按取消语义结算这张卡(取消 = run 死:绝不重新 park,卡 expire),于是写在终局上的那句话与可操作面
|
|
83
|
+
* 不一致 —— 文案说可再续,待办队列里没有这张卡、同一张卡再决断 404。取消不可逆(契约),所以真话是这一句。
|
|
84
|
+
* 引擎那句里「放回 pending」的事实与「同一批准可再续」的结论都是**引擎当时**的,本服务在其后改了卡的状态,最后一个写手的句子才对。
|
|
85
|
+
*/
|
|
86
|
+
export declare const CARD_SETTLED_BY_CANCEL_MESSAGE = "Cancelled \u2014 the approved action never started, and the cancel settled this approval: it cannot be resumed (start a new run to continue)";
|
|
87
|
+
/**
|
|
88
|
+
* 一条被取消的续跑腿的失败终局,在本服务亲手 expire 了它那张卡之后的形:**只换文案**,码(引擎补的 `resume.reopened_unstarted` 等)、成因位、统计与其余字段原样
|
|
89
|
+
* (码立住是成文契约;换码是另一条 wire 变更)。非 `failed` 终局原样返回。纯函数,不改入参。
|
|
90
|
+
* 调用点只有 `http/resume-legs.ts` 的 `reopenedAfterLeg`(判据 = 店的事实「本座 expire 了一张 pending 的卡」,不嗅引擎文案)。
|
|
91
|
+
*/
|
|
92
|
+
export declare function withCardSettledByCancelMessage<R extends {
|
|
93
|
+
terminal: TerminalCause;
|
|
94
|
+
}>(leg: R): R;
|
|
61
95
|
/** {@link mintCancelledResult} 的公共实参(`engineResult` 另列:在场与否决定返回型)。 */
|
|
62
96
|
interface CancelMintArgs {
|
|
63
97
|
/** 这条 run 的账本 id(活性登记的键;账本不在场时给空串,快照按「证不出」处理)。 */
|
|
@@ -36,6 +36,13 @@ export function mintCancelledResult(args) {
|
|
|
36
36
|
abortCause: args.cause,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
export const CARD_SETTLED_BY_CANCEL_MESSAGE = "Cancelled — the approved action never started, and the cancel settled this approval: it cannot be resumed (start a new run to continue)";
|
|
40
|
+
export function withCardSettledByCancelMessage(leg) {
|
|
41
|
+
const t = leg.terminal;
|
|
42
|
+
if (t.kind !== "failed")
|
|
43
|
+
return leg;
|
|
44
|
+
return { ...leg, terminal: { ...t, message: CARD_SETTLED_BY_CANCEL_MESSAGE } };
|
|
45
|
+
}
|
|
39
46
|
export function buildCancelledLedgerRow(c) {
|
|
40
47
|
const t = c.terminal;
|
|
41
48
|
return {
|
package/dist/run-local.js
CHANGED
|
@@ -770,14 +770,8 @@ export async function runLocal(argv, deps = {}) {
|
|
|
770
770
|
}
|
|
771
771
|
if (result.terminal.kind !== "completed" || unfinishedStop !== undefined)
|
|
772
772
|
exitCode = 1;
|
|
773
|
-
if (memorySync)
|
|
774
|
-
|
|
775
|
-
await memorySync.syncOnce("post-task");
|
|
776
|
-
}
|
|
777
|
-
catch (err) {
|
|
778
|
-
logger.warn("memory_sync_failed", { reason: "post-task", err: String(err) });
|
|
779
|
-
}
|
|
780
|
-
}
|
|
773
|
+
if (memorySync)
|
|
774
|
+
await memorySync.syncOnce("post-task");
|
|
781
775
|
await settleSessionBackground();
|
|
782
776
|
runner.sessions.dispose();
|
|
783
777
|
subRunner.sessions.dispose();
|