@sema-agent/server 7.141.0 → 7.143.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 +657 -0
- package/USAGE.md +102 -30
- package/dist/adoption/plan.js +22 -0
- package/dist/approval-ask-audit-store.d.ts +61 -43
- package/dist/approval-ask-audit-store.js +69 -44
- package/dist/approval-ask-machine.d.ts +15 -7
- package/dist/approval-ask-machine.js +10 -4
- package/dist/approval-card.d.ts +126 -27
- package/dist/approval-card.js +103 -23
- 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-queue-kind.d.ts +27 -0
- package/dist/approval-queue-kind.js +6 -0
- package/dist/approval-reconciler.d.ts +41 -9
- package/dist/approval-reconciler.js +45 -42
- package/dist/audit.js +4 -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 +17 -1
- package/dist/boot/budget-tracing.js +10 -6
- package/dist/boot/device-lane.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +24 -4
- package/dist/boot/execution-env.js +26 -23
- package/dist/boot/execution-lane.js +2 -1
- package/dist/boot/exit-boundary.js +5 -4
- package/dist/boot/governance-seams.d.ts +26 -41
- package/dist/boot/governance-seams.js +61 -28
- package/dist/boot/late-binds.d.ts +7 -4
- package/dist/boot/late-binds.js +3 -1
- package/dist/boot/leader.js +2 -4
- package/dist/boot/memory-consolidation.d.ts +0 -8
- package/dist/boot/memory-consolidation.js +5 -4
- 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.js +9 -7
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +19 -10
- package/dist/boot/retention-lane.js +5 -3
- package/dist/boot/runner-deps.d.ts +10 -7
- package/dist/boot/runner-deps.js +20 -29
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.js +4 -3
- package/dist/boot/shutdown.d.ts +9 -0
- package/dist/boot/shutdown.js +19 -4
- package/dist/boot/stage-02-web-admin-pull.js +3 -0
- package/dist/boot/stage-03-store-backend.d.ts +4 -1
- package/dist/boot/stage-03-store-backend.js +1 -2
- package/dist/boot/stage-04-stores.d.ts +5 -2
- package/dist/boot/stage-04-stores.js +6 -7
- package/dist/boot/stage-05-execution-env.d.ts +6 -3
- package/dist/boot/stage-05-execution-env.js +8 -6
- package/dist/boot/stage-06-runners.d.ts +6 -3
- package/dist/boot/stage-06-runners.js +3 -4
- package/dist/boot/stage-07-capability-layer.d.ts +6 -3
- package/dist/boot/stage-07-capability-layer.js +7 -2
- package/dist/boot/stage-08-reapers.d.ts +6 -3
- package/dist/boot/stage-08-reapers.js +3 -2
- package/dist/boot/stage-09-leader.d.ts +6 -3
- package/dist/boot/stage-10-http-server.d.ts +6 -3
- package/dist/boot/stage-10-http-server.js +14 -5
- package/dist/boot/stores.d.ts +5 -2
- package/dist/boot/stores.js +10 -9
- package/dist/boot/web-admin.d.ts +5 -1
- package/dist/boot/web-admin.js +48 -36
- 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/hands-lane.d.ts +9 -39
- package/dist/capabilities/hands-lane.js +8 -13
- package/dist/capabilities/mcp-probe.d.ts +3 -2
- package/dist/capabilities/scenarios.d.ts +11 -1
- package/dist/capabilities/scenarios.js +1 -0
- package/dist/capabilities/skills.d.ts +1 -17
- package/dist/capabilities/skills.js +0 -8
- package/dist/capability-facts.d.ts +31 -7
- package/dist/capability-facts.js +27 -4
- package/dist/card-void.d.ts +6 -9
- package/dist/config-catalog.js +2 -1
- package/dist/config-dir-trust.js +58 -29
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +7 -0
- package/dist/config-provider.d.ts +10 -0
- package/dist/config-provider.js +15 -4
- package/dist/config-root-migration.d.ts +13 -0
- package/dist/config-root-migration.js +99 -0
- package/dist/config-types.d.ts +19 -8
- package/dist/config.d.ts +10 -3
- package/dist/config.js +18 -4
- package/dist/delegated-session-owner.d.ts +5 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/env-model-table-gate.d.ts +53 -0
- package/dist/env-model-table-gate.js +53 -0
- package/dist/execution-lane-caps.d.ts +2 -1
- package/dist/fleet/fleet-bus.d.ts +34 -55
- package/dist/fleet/fleet-bus.js +35 -100
- package/dist/fleet/fleet-reconciler.d.ts +4 -4
- package/dist/fleet/fleet-reconciler.js +1 -1
- package/dist/fleet/subagent-tail-bus.d.ts +14 -11
- package/dist/fleet/subagent-tail-bus.js +15 -28
- package/dist/hooks/hook-runner.d.ts +6 -0
- package/dist/hooks/hook-runner.js +58 -8
- package/dist/host-decision.d.ts +54 -20
- package/dist/host-decision.js +42 -14
- package/dist/http/admission.d.ts +1 -1
- package/dist/http/admission.js +7 -3
- package/dist/http/approval-sweeps.js +11 -8
- package/dist/http/caller-trust.d.ts +1 -1
- package/dist/http/dispatch.js +4 -2
- package/dist/http/durable-run-leg.js +2 -2
- package/dist/http/engine-refusal.d.ts +16 -0
- package/dist/http/engine-refusal.js +17 -0
- package/dist/http/execution-context.d.ts +2 -2
- package/dist/http/idempotency.d.ts +1 -1
- package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
- package/dist/http/memory-compliance-failure-reply.js +4 -6
- package/dist/http/owner-access.d.ts +11 -0
- package/dist/http/owner-access.js +13 -0
- package/dist/http/principal-gate.d.ts +3 -1
- package/dist/http/principal-gate.js +17 -8
- package/dist/http/resume-legs.d.ts +21 -7
- package/dist/http/resume-legs.js +76 -58
- package/dist/http/resume-restatement.d.ts +39 -0
- package/dist/http/resume-restatement.js +25 -0
- package/dist/http/route-ctx.d.ts +35 -10
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/approvals-assistant.d.ts +9 -1
- package/dist/http/routes/approvals-assistant.js +142 -31
- package/dist/http/routes/attachments.js +18 -0
- package/dist/http/routes/capabilities.js +16 -4
- package/dist/http/routes/memory-policy.d.ts +2 -2
- package/dist/http/routes/memory-policy.js +27 -7
- package/dist/http/routes/notify-wake.js +23 -25
- package/dist/http/routes/rules.js +48 -13
- package/dist/http/routes/runs.js +12 -11
- package/dist/http/routes/session-memory-sync.d.ts +91 -0
- package/dist/http/routes/session-memory-sync.js +174 -0
- package/dist/http/routes/session-sync.js +13 -20
- package/dist/http/routes/sessions-list.d.ts +2 -1
- package/dist/http/routes/sessions.d.ts +2 -2
- package/dist/http/routes/sessions.js +22 -23
- package/dist/http/routes/tasks.js +34 -54
- package/dist/http/run-read-face.d.ts +7 -0
- package/dist/http/run-read-face.js +9 -3
- package/dist/http/send.d.ts +5 -0
- package/dist/http/send.js +9 -2
- package/dist/http/server.d.ts +27 -15
- package/dist/http/server.js +4 -6
- package/dist/http/stored-body-replay.js +15 -4
- 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/http/wire-types.d.ts +17 -7
- package/dist/identity-source.d.ts +21 -2
- package/dist/identity-source.js +6 -0
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.d.ts +7 -1
- package/dist/leader/wire.js +6 -5
- package/dist/leg-facts.d.ts +37 -0
- package/dist/leg-facts.js +15 -0
- package/dist/memory-bundle-engine.d.ts +1 -4
- package/dist/memory-bundle-engine.js +2 -2
- package/dist/memory-incident-sink.d.ts +19 -6
- package/dist/memory-incident-sink.js +8 -4
- package/dist/memory-operator-faces.d.ts +30 -66
- package/dist/memory-operator-faces.js +17 -88
- package/dist/memory-scope-stores.d.ts +38 -40
- package/dist/memory-scope-stores.js +51 -36
- package/dist/memory-scope.d.ts +0 -9
- package/dist/memory-scope.js +0 -9
- package/dist/memory-sync-client.d.ts +21 -21
- package/dist/memory-sync-client.js +62 -43
- package/dist/memory-sync-identity.d.ts +24 -0
- package/dist/memory-sync-identity.js +57 -0
- package/dist/memory-sync.d.ts +27 -5
- package/dist/memory-sync.js +29 -7
- package/dist/model-plane-face.d.ts +13 -7
- package/dist/model-plane-face.js +2 -1
- package/dist/observability/fail-open.d.ts +47 -14
- package/dist/observability/fail-open.js +37 -11
- package/dist/observability/run-terminal.d.ts +92 -34
- package/dist/observability/run-terminal.js +171 -162
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
- package/dist/orchestration/workflow-completion-inbox.js +0 -12
- package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
- package/dist/orchestration/workflow-park-decision.d.ts +8 -3
- package/dist/orchestration/workflow-park-decision.js +5 -3
- package/dist/org-delivery-gate.d.ts +11 -0
- package/dist/org-delivery-gate.js +19 -1
- package/dist/parked-decide.d.ts +13 -9
- package/dist/parked-decide.js +7 -7
- package/dist/parked-revive-surface.js +1 -1
- package/dist/peer-inbound-face.d.ts +127 -0
- package/dist/peer-inbound-face.js +158 -0
- package/dist/permission-rule-list.d.ts +13 -9
- package/dist/permission-rule-list.js +7 -3
- package/dist/plugins/approval-ask-store-file.d.ts +14 -4
- package/dist/plugins/approval-ask-store-file.js +64 -11
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +17 -14
- package/dist/plugins/approval-ask-store-sql.d.ts +70 -23
- package/dist/plugins/approval-ask-store-sql.js +79 -30
- package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
- package/dist/plugins/checkpoint-store-sql.js +9 -7
- package/dist/plugins/file-history-store-sql.d.ts +24 -18
- package/dist/plugins/file-history-store-sql.js +24 -19
- package/dist/plugins/file-run-store.d.ts +35 -1
- package/dist/plugins/file-run-store.js +108 -9
- package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
- package/dist/plugins/imported-layer-root-retirement.js +96 -0
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +2 -2
- package/dist/plugins/local-session-store.d.ts +42 -9
- package/dist/plugins/local-session-store.js +83 -33
- package/dist/plugins/local-task-attachment-store.d.ts +12 -2
- package/dist/plugins/local-task-attachment-store.js +67 -43
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
- package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
- package/dist/plugins/mailbox-store-sql.d.ts +12 -8
- package/dist/plugins/mailbox-store-sql.js +76 -7
- package/dist/plugins/memory-key-guards.d.ts +3 -3
- package/dist/plugins/memory-run-store.d.ts +6 -1
- package/dist/plugins/memory-run-store.js +11 -1
- package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
- package/dist/plugins/memory-sync-store-pg.js +21 -0
- package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
- package/dist/plugins/memory-sync-store-tidb.js +13 -1
- package/dist/plugins/one-time-migrations.d.ts +58 -2
- package/dist/plugins/one-time-migrations.js +161 -4
- package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
- package/dist/plugins/org-delivery-quota-store.js +2 -12
- package/dist/plugins/park-commit-watch.d.ts +1 -1
- package/dist/plugins/permission-rule-store-file.d.ts +10 -1
- package/dist/plugins/permission-rule-store-file.js +15 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
- package/dist/plugins/permission-rule-store-sql.js +35 -2
- package/dist/plugins/remote-env-device.d.ts +8 -11
- package/dist/plugins/remote-env-device.js +6 -3
- package/dist/plugins/remote-env-e2b.d.ts +23 -4
- package/dist/plugins/remote-env-e2b.js +111 -28
- package/dist/plugins/remote-env-k8s.d.ts +61 -22
- package/dist/plugins/remote-env-k8s.js +133 -33
- package/dist/plugins/remote-env-local-docker.d.ts +15 -3
- package/dist/plugins/remote-env-local-docker.js +40 -12
- package/dist/plugins/retention-store-sql.d.ts +2 -2
- package/dist/plugins/run-store-sql.d.ts +9 -1
- package/dist/plugins/run-store-sql.js +9 -1
- package/dist/plugins/sandbox-read-floor.d.ts +5 -7
- package/dist/plugins/sandbox-read-floor.js +2 -7
- package/dist/plugins/sql-errors.d.ts +10 -1
- package/dist/plugins/sql-errors.js +13 -0
- package/dist/plugins/store-backend.d.ts +33 -35
- package/dist/plugins/store-backend.js +10 -1
- package/dist/plugins/store-contracts.d.ts +50 -1
- package/dist/plugins/store-contracts.js +6 -0
- package/dist/plugins/task-attachment-store.d.ts +112 -14
- package/dist/plugins/task-attachment-store.js +207 -29
- package/dist/plugins/tool-result-store-sql.d.ts +9 -8
- package/dist/plugins/tool-result-store-sql.js +23 -2
- package/dist/plugins/wiring-section-codec.d.ts +32 -0
- package/dist/plugins/wiring-section-codec.js +55 -0
- 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/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +5 -1
- package/dist/rules-consent.d.ts +28 -5
- package/dist/rules-consent.js +40 -28
- package/dist/run-local.js +11 -13
- package/dist/runs.d.ts +12 -8
- package/dist/runs.js +6 -8
- package/dist/runtime-governance.d.ts +1 -1
- 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 +9 -0
- package/dist/security.js +8 -0
- package/dist/server-secret-env.d.ts +4 -0
- package/dist/session-error-wire.d.ts +36 -0
- package/dist/session-error-wire.js +13 -0
- package/dist/session-incarnation.d.ts +1 -1
- package/dist/session-notify-door.d.ts +106 -0
- package/dist/session-notify-door.js +93 -0
- package/dist/session-sync.d.ts +9 -14
- package/dist/session-sync.js +5 -5
- package/dist/session-workspace.d.ts +3 -13
- package/dist/session-workspace.js +0 -9
- package/dist/standing-answer-grant.d.ts +5 -0
- package/dist/standing-answer-grant.js +10 -0
- package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
- package/dist/stores-sql/session-policy-store-sql.js +3 -13
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +21 -10
- package/dist/task-settings.js +46 -13
- package/dist/terminal.d.ts +39 -19
- package/dist/terminal.js +65 -22
- package/dist/tool-approval.d.ts +56 -37
- package/dist/tool-approval.js +78 -71
- package/dist/trace/core-keyset-guard.d.ts +134 -14
- package/dist/trace/engine-notice-wire.d.ts +9 -3
- package/dist/trace/engine-notice-wire.js +7 -3
- package/dist/trace/exit-seat.d.ts +9 -2
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +10 -6
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/model-segment.d.ts +55 -25
- package/dist/trace/model-segment.js +101 -33
- package/dist/trace/project.d.ts +25 -5
- package/dist/trace/project.js +22 -0
- package/dist/trace/redact.d.ts +61 -9
- package/dist/trace/redact.js +1490 -468
- package/dist/trace/self-minted-wire.d.ts +745 -24
- package/dist/trace/self-minted-wire.js +239 -9
- package/dist/trace/wire-projection-faces.d.ts +2 -2
- package/dist/trace/wire-projection-faces.js +2 -2
- package/dist/verifier-pause-card.d.ts +33 -5
- package/dist/verifier-pause-card.js +15 -10
- package/dist/web-admin-client/apply-effective.d.ts +12 -16
- package/dist/web-admin-client/apply-effective.js +109 -9
- package/dist/web-admin-client/effective-keys.js +2 -0
- package/dist/web-admin-client/types.d.ts +4 -0
- package/dist/workspace-space.d.ts +19 -3
- package/dist/workspace-space.js +8 -4
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
- package/package.json +3 -2
- package/wire-changes/server@7.142.0.json +639 -0
- package/wire-changes/server@7.143.0.json +1167 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** 项目包的 mime(厂商类型;格式由类型定死 = gzip 的 POSIX tar)。普通 tar / gzip 用普通 mime 上传就仍是附件。 */
|
|
2
|
+
export declare const PROJECT_BUNDLE_MIME = "application/vnd.sema.project-bundle";
|
|
3
|
+
/** 一只附件行的种类。 */
|
|
4
|
+
export type AttachmentKind = "attachment" | "project_bundle";
|
|
5
|
+
/** 附件行种类的唯一判据:已存的 mime(一个事实一格 —— 内容类型本就在 mime 列)。 */
|
|
6
|
+
export declare function attachmentKindOf(mime: string): AttachmentKind;
|
|
7
|
+
/** `attachment.bundle_invalid` 拒体 `reason` 的闭集(wire 值;登记 = `trace/self-minted-wire.ts` 的 `projectBundleInvalidReason`)。 */
|
|
8
|
+
export declare const PROJECT_BUNDLE_INVALID_REASONS: readonly ["not_archive", "unsafe_path", "entry_type", "reserved_path", "duplicate_path", "expanded_too_large", "too_many_entries"];
|
|
9
|
+
export type ProjectBundleInvalidReason = (typeof PROJECT_BUNDLE_INVALID_REASONS)[number];
|
|
10
|
+
/** 展开字节上限(解压后的 tar 流,含头与填充)。 */
|
|
11
|
+
export declare const PROJECT_BUNDLE_MAX_EXPANDED_BYTES: number;
|
|
12
|
+
/** 解出来的文件系统对象上限(普通文件 + 目录 + 符号链接 + 路径隐含的目录;归档根 `./` 不计)。 */
|
|
13
|
+
export declare const PROJECT_BUNDLE_MAX_ENTRIES = 200000;
|
|
14
|
+
/** 顶层保留名:`.git` 走 git 引用那条路、`attachments` 是附件物化的落点 —— 初始一刻一棵子树一个写者。 */
|
|
15
|
+
export declare const PROJECT_BUNDLE_RESERVED_TOP_LEVEL: readonly string[];
|
|
16
|
+
/** 一次读的结局。成功形:条目数、顶层名(播种前判「撞上已有路径」用)、按请求给出的规范归档(gzip 的 ustar)。 */
|
|
17
|
+
export type ProjectBundleReading = {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly entries: number;
|
|
20
|
+
readonly topLevel: readonly string[];
|
|
21
|
+
readonly canonical?: Uint8Array;
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly reason: ProjectBundleInvalidReason;
|
|
25
|
+
readonly path?: string;
|
|
26
|
+
readonly detail: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* 读一份字节是不是项目包(见模块头的规则)。`canonical: true` ⇒ 同时交出规范归档(播种用;上传只判不铸)。`limits` 只给钉换小上限。从不抛。
|
|
30
|
+
*/
|
|
31
|
+
export declare function readProjectBundle(bytes: Uint8Array, opts?: {
|
|
32
|
+
readonly canonical?: boolean;
|
|
33
|
+
readonly limits?: {
|
|
34
|
+
readonly maxExpandedBytes?: number;
|
|
35
|
+
readonly maxEntries?: number;
|
|
36
|
+
};
|
|
37
|
+
}): Promise<ProjectBundleReading>;
|
|
38
|
+
//# sourceMappingURL=project-bundle.d.ts.map
|
|
@@ -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
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* **规则条目表在本仓受理口的判** —— 一份,四张表同调(core 7.42.0 起「这个调用不用问」只有一个去处:引擎的 allow 层;条目的文法、匹配、
|
|
3
3
|
* 名字、读成空的通告都归引擎,本仓不写第二套):
|
|
4
4
|
* · 请求自带技能的 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代 `agents[].skills[]` 两处受理口,`spec-fields.ts` / `http/admission.ts`);
|
|
5
|
-
* · 请求的 `settings.permissions.allow`(
|
|
5
|
+
* · 请求的 `settings.permissions.allow`(整份按位置投 `TaskSpec.allowedTools`,`task-settings.ts`;条目也可以是设置层的 `{ rule, layer }`,本文件按它的规则原文读,层只选锚);
|
|
6
6
|
* · 运维的 `APPROVAL_GRANT`(整份投 `RunnerDeps.allowedTools`,`config.ts` 启动校验)。
|
|
7
7
|
*
|
|
8
8
|
* 本仓在这些表上只剩下面几问(内容形、`*`、写路径形、名字引擎怎么读就怎么算):
|
|
9
|
-
* ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)
|
|
9
|
+
* ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)。请求的 allow 桶的形另由桶门判
|
|
10
|
+
* (`task-settings.ts` 的 `settingsPermissionBucketIssue`:串或带层条目),它进下面 ③ 时带层条目读成规则原文({@link toolListsOf})。
|
|
10
11
|
* 本谓词不带条数帽(引擎的 `screenAllowedToolsValue` 只判形);条数帽是另一只函数 {@link allowLayerEntryCountIssue}(`MAX_ALLOW_LAYER_ENTRIES`,
|
|
11
12
|
* 请求 allow 桶、技能两只清单、运维表四张表同一只、同一句)。
|
|
12
13
|
* ② (7.116.0–7.122.x 的「引擎读不到的一形」—— 工具段为空的协议名 `mcp__srv__` 四张表都拒 —— 随 core 7.48.0 [ref] 退役:引擎判官给它判词
|
package/dist/rule-entry-list.js
CHANGED
|
@@ -3,6 +3,7 @@ import { displayListEntry } from "./list-entry-display.js";
|
|
|
3
3
|
import { limitRefusal } from "./limit-refusal.js";
|
|
4
4
|
import { TOOL_NAME_ROSTER } from "./namespaced-rule-name.js";
|
|
5
5
|
import { NOT_IN_ROSTER, entryProblem, entryProblemsFacts } from "./entry-problems.js";
|
|
6
|
+
import { layeredRuleOk } from "./permission-rule-list.js";
|
|
6
7
|
export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
7
8
|
export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
|
|
8
9
|
const ALLOW_LAYER_OVER_CAP_TAIL = "Refused rather than truncated: a truncated list silently drops the entries past the cap. Merge entries into broader ones (mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**)) or trim the list";
|
|
@@ -162,7 +163,10 @@ function* toolListsOf(body) {
|
|
|
162
163
|
const settings = body["settings"];
|
|
163
164
|
const permissions = isPlainRecord(settings) ? settings["permissions"] : undefined;
|
|
164
165
|
if (isPlainRecord(permissions))
|
|
165
|
-
yield { layer: "settings.permissions.allow", field: "settings.permissions.allow", prefix: "", at: "settings.permissions.allow", list: permissions["allow"] };
|
|
166
|
+
yield { layer: "settings.permissions.allow", field: "settings.permissions.allow", prefix: "", at: "settings.permissions.allow", list: ruleTextsOfBucket(permissions["allow"]) };
|
|
167
|
+
}
|
|
168
|
+
function ruleTextsOfBucket(list) {
|
|
169
|
+
return Array.isArray(list) ? list.map((x) => (layeredRuleOk(x) ? x.rule : x)) : list;
|
|
166
170
|
}
|
|
167
171
|
function inertReadingOfList(l) {
|
|
168
172
|
const face = TOOL_LIST_FACES[l.layer];
|