@sema-agent/server 7.132.0 → 7.133.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 +266 -0
- package/README.md +8 -9
- package/README.zh-CN.md +7 -6
- package/USAGE.md +174 -55
- package/deploy/sema-up/README.md +19 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -2
- package/deploy/sema-up/chart/templates/server.yaml +5 -2
- package/deploy/sema-up/stack/compose.yaml +3 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -2
- package/dist/adoption/plan.d.ts +3 -2
- package/dist/adoption/plan.js +7 -5
- package/dist/adoption/runner.js +3 -10
- package/dist/adoption/sql.d.ts +3 -2
- package/dist/adoption/sql.js +3 -5
- package/dist/adoption/wire.d.ts +4 -1
- package/dist/adoption/wire.js +1 -0
- package/dist/approval-ask-audit-store.d.ts +31 -13
- package/dist/approval-ask-audit-store.js +272 -74
- package/dist/approval-ask-machine.d.ts +15 -0
- package/dist/approval-ask-machine.js +6 -0
- package/dist/approval-ask-retention.d.ts +136 -0
- package/dist/approval-ask-retention.js +102 -0
- package/dist/approval-card.d.ts +79 -19
- package/dist/approval-card.js +44 -13
- package/dist/approval-deny-reasons.d.ts +1 -1
- package/dist/approval.d.ts +9 -0
- package/dist/approval.js +24 -3
- package/dist/audit.d.ts +27 -10
- package/dist/audit.js +12 -16
- package/dist/bake-runner/main.js +3 -2
- package/dist/boot/deferred-sandbox-path-env.js +1 -1
- package/dist/boot/execution-env.js +9 -7
- package/dist/boot/execution-lane.js +11 -0
- package/dist/boot/leader.js +2 -0
- package/dist/boot/memory-boundary.js +2 -2
- package/dist/boot/memory-consolidation.d.ts +5 -4
- package/dist/boot/memory-consolidation.js +7 -6
- package/dist/boot/org-delivery.d.ts +28 -0
- package/dist/boot/org-delivery.js +20 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +21 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +60 -81
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/session-faces.d.ts +1 -0
- package/dist/boot/session-faces.js +4 -2
- package/dist/boot/stage-02-web-admin-pull.js +4 -1
- package/dist/boot/stage-06-runners.d.ts +2 -0
- package/dist/boot/stage-06-runners.js +6 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -0
- package/dist/boot/stage-07-capability-layer.js +24 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -0
- package/dist/boot/stage-10-http-server.d.ts +3 -0
- package/dist/boot/stage-10-http-server.js +5 -6
- package/dist/boot/stores.js +2 -2
- package/dist/boot/web-admin.d.ts +4 -0
- package/dist/boot/web-admin.js +151 -63
- package/dist/boot/workflow-orchestration.d.ts +2 -0
- package/dist/boot/workflow-orchestration.js +2 -1
- package/dist/brain.js +8 -3
- package/dist/breaker-key.d.ts +6 -0
- package/dist/breaker-key.js +29 -0
- package/dist/caller-text-echo.d.ts +9 -6
- package/dist/caller-text-echo.js +8 -11
- package/dist/capabilities/center-plugins.d.ts +7 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/execution-lane.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +21 -0
- package/dist/capabilities/hands-lane.js +10 -0
- package/dist/capabilities/mcp-probe.d.ts +36 -4
- package/dist/capabilities/mcp-probe.js +27 -4
- package/dist/capabilities/scenarios.d.ts +63 -7
- package/dist/capabilities/scenarios.js +39 -7
- package/dist/capability-facts.d.ts +34 -4
- package/dist/capability-facts.js +28 -2
- package/dist/center-config-tier.d.ts +69 -0
- package/dist/center-config-tier.js +42 -0
- package/dist/checkpoint-identity.d.ts +16 -0
- package/dist/checkpoint-identity.js +19 -0
- package/dist/config-catalog.d.ts +17 -10
- package/dist/config-catalog.js +43 -28
- package/dist/config-d-migration.d.ts +3 -1
- package/dist/config-d-migration.js +2 -2
- package/dist/config-dir-trust.d.ts +114 -0
- package/dist/config-dir-trust.js +388 -0
- package/dist/config-invariants.d.ts +18 -5
- package/dist/config-invariants.js +35 -3
- package/dist/config-provider.d.ts +6 -1
- package/dist/config-provider.js +6 -2
- package/dist/config-types.d.ts +71 -14
- package/dist/config-types.js +8 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +121 -59
- package/dist/delegated-session-owner.d.ts +185 -0
- package/dist/delegated-session-owner.js +119 -0
- package/dist/device-protocol.d.ts +5 -3
- package/dist/device-protocol.js +1 -1
- package/dist/device-ws-hub.js +21 -2
- package/dist/device-ws-protocol.d.ts +70 -6
- package/dist/device-ws-protocol.js +50 -23
- package/dist/elicitation.js +3 -2
- package/dist/entry-problems.d.ts +34 -0
- package/dist/entry-problems.js +14 -0
- package/dist/execution-lane-caps.d.ts +49 -10
- package/dist/execution-lane-caps.js +43 -4
- package/dist/field-conflict.d.ts +43 -0
- package/dist/field-conflict.js +20 -0
- package/dist/fleet/fleet-bus.d.ts +80 -6
- package/dist/fleet/fleet-bus.js +41 -9
- package/dist/hooks/classic-stations.d.ts +10 -5
- package/dist/hooks/classic-stations.js +4 -4
- package/dist/hooks/command-spawn.d.ts +57 -0
- package/dist/hooks/command-spawn.js +139 -0
- package/dist/hooks/hook-llm.js +2 -2
- package/dist/hooks/hook-runner.d.ts +127 -25
- package/dist/hooks/hook-runner.js +668 -287
- package/dist/hooks/hook-source.d.ts +44 -0
- package/dist/hooks/hook-source.js +40 -0
- package/dist/http/active-run-conflict.d.ts +5 -13
- package/dist/http/active-run-conflict.js +17 -27
- package/dist/http/admission.js +103 -60
- package/dist/http/approval-sweeps.js +55 -18
- package/dist/http/caller-trust.d.ts +77 -0
- package/dist/http/caller-trust.js +143 -0
- package/dist/http/checkpoint-ctx.d.ts +16 -16
- package/dist/http/checkpoint-ctx.js +13 -10
- package/dist/http/claimed-run.d.ts +4 -3
- package/dist/http/device-placement.d.ts +2 -4
- package/dist/http/device-placement.js +2 -1
- package/dist/http/dispatch.js +10 -4
- package/dist/http/durable-run-leg.d.ts +4 -32
- package/dist/http/durable-run-leg.js +34 -10
- package/dist/http/owner-access.d.ts +56 -0
- package/dist/http/owner-access.js +42 -0
- package/dist/http/principal-gate.d.ts +48 -23
- package/dist/http/principal-gate.js +39 -12
- package/dist/http/readiness.d.ts +11 -5
- package/dist/http/readiness.js +10 -3
- package/dist/http/request-guards.d.ts +16 -23
- package/dist/http/request-guards.js +30 -15
- package/dist/http/resume-ctx-read.d.ts +2 -5
- package/dist/http/resume-ctx-read.js +7 -4
- package/dist/http/resume-legs.d.ts +3 -4
- package/dist/http/resume-legs.js +126 -69
- package/dist/http/retired-service-deps.d.ts +43 -0
- package/dist/http/retired-service-deps.js +18 -0
- package/dist/http/route-ctx.d.ts +44 -28
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/a2a-serve.js +3 -4
- package/dist/http/routes/admin-config-refresh.js +1 -1
- package/dist/http/routes/admin-drain.js +1 -1
- package/dist/http/routes/admin-memory-optout.js +5 -1
- package/dist/http/routes/adoption.js +2 -9
- package/dist/http/routes/approvals-assistant.js +49 -48
- package/dist/http/routes/background.js +3 -2
- package/dist/http/routes/capabilities.js +20 -4
- package/dist/http/routes/config-catalog.d.ts +3 -3
- package/dist/http/routes/config-catalog.js +1 -1
- package/dist/http/routes/devices.js +3 -17
- package/dist/http/routes/diagnostics.js +5 -1
- package/dist/http/routes/images.js +6 -7
- package/dist/http/routes/leader.js +1 -8
- package/dist/http/routes/mcp-reconnect.d.ts +1 -1
- package/dist/http/routes/mcp-reconnect.js +3 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/memory-policy.d.ts +1 -1
- package/dist/http/routes/memory-policy.js +3 -2
- package/dist/http/routes/notify-wake.js +8 -21
- package/dist/http/routes/observability.js +2 -2
- package/dist/http/routes/principal-inbox.d.ts +39 -0
- package/dist/http/routes/principal-inbox.js +76 -0
- package/dist/http/routes/retention-ops.js +2 -9
- package/dist/http/routes/rules.js +5 -18
- package/dist/http/routes/runs.d.ts +5 -0
- package/dist/http/routes/runs.js +33 -106
- package/dist/http/routes/session-sync.js +15 -28
- package/dist/http/routes/sessions.js +30 -40
- package/dist/http/routes/side-query.js +6 -4
- package/dist/http/routes/tasks.js +50 -14
- package/dist/http/routes/workflows.d.ts +351 -0
- package/dist/http/routes/workflows.js +153 -119
- package/dist/http/send.d.ts +24 -3
- package/dist/http/send.js +14 -2
- package/dist/http/server.d.ts +17 -18
- package/dist/http/server.js +15 -13
- package/dist/http/stored-body-replay.js +17 -4
- package/dist/http/submit-ctx.js +4 -3
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +5 -4
- package/dist/http/wire-types.d.ts +10 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/lane-home-probe.d.ts +78 -9
- package/dist/lane-home-probe.js +78 -12
- package/dist/leader/endpoint.d.ts +4 -1
- package/dist/leader/endpoint.js +11 -10
- package/dist/limit-refusal.d.ts +17 -5
- package/dist/limit-refusal.js +4 -1
- package/dist/mcp-assertion-header.d.ts +31 -10
- package/dist/mcp-assertion-header.js +10 -3
- package/dist/mcp-tool-faces.js +1 -3
- package/dist/memory-project-layer.d.ts +37 -0
- package/dist/memory-project-layer.js +46 -0
- package/dist/memory-scope.d.ts +23 -16
- package/dist/memory-scope.js +24 -8
- package/dist/memory-sync-client.d.ts +27 -13
- package/dist/memory-sync-client.js +107 -18
- package/dist/model-entry-refusal.d.ts +9 -1
- package/dist/model-entry-refusal.js +6 -1
- package/dist/model-plane-face.d.ts +51 -0
- package/dist/model-plane-face.js +24 -0
- package/dist/model-provider.d.ts +5 -0
- package/dist/model-provider.js +4 -2
- package/dist/model-route-endpoint.d.ts +8 -11
- package/dist/model-route-endpoint.js +4 -7
- package/dist/model-route-env.d.ts +22 -0
- package/dist/model-route-env.js +2 -0
- package/dist/model-route-gap.d.ts +60 -30
- package/dist/model-route-gap.js +21 -15
- package/dist/observability/corrupt-read-seat.d.ts +7 -0
- package/dist/observability/corrupt-read-seat.js +9 -0
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +18 -4
- package/dist/observability/fail-open.d.ts +35 -9
- package/dist/observability/fail-open.js +35 -9
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/prompt-manifest.d.ts +3 -17
- package/dist/observability/prompt-manifest.js +45 -42
- package/dist/observability/tool-trace.js +9 -9
- package/dist/operator-ask.d.ts +10 -0
- package/dist/operator-ask.js +3 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +12 -12
- package/dist/orchestration/workflow-agent-session-index.js +9 -13
- package/dist/orchestration/workflow-completion-inbox.d.ts +42 -15
- package/dist/orchestration/workflow-completion-inbox.js +13 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +13 -32
- package/dist/orchestration/workflow-notify-journal.js +27 -34
- package/dist/orchestration/workflow-park-decision.d.ts +89 -0
- package/dist/orchestration/workflow-park-decision.js +118 -0
- package/dist/org-delivery-gate.d.ts +81 -0
- package/dist/org-delivery-gate.js +59 -0
- package/dist/panel-tool-names.d.ts +23 -0
- package/dist/panel-tool-names.js +8 -0
- package/dist/parked-decide.d.ts +39 -6
- package/dist/parked-decide.js +98 -51
- package/dist/parked-revive-surface.d.ts +9 -2
- package/dist/parked-revive-surface.js +11 -8
- package/dist/permission-rule-list.d.ts +1 -1
- package/dist/permission-rule-list.js +2 -2
- package/dist/plugins/approval-ask-store-file.d.ts +16 -1
- package/dist/plugins/approval-ask-store-file.js +21 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +18 -1
- package/dist/plugins/approval-ask-store-memory.js +34 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +25 -0
- package/dist/plugins/approval-ask-store-sql.js +37 -1
- package/dist/plugins/blob-backend.d.ts +38 -25
- package/dist/plugins/blob-backend.js +29 -19
- package/dist/plugins/breaker-state-sql.d.ts +2 -2
- package/dist/plugins/breaker-state-sql.js +4 -4
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +20 -2
- package/dist/plugins/file-history-store-sql.d.ts +5 -2
- package/dist/plugins/file-history-store-sql.js +3 -1
- package/dist/plugins/file-run-store.d.ts +5 -13
- package/dist/plugins/file-run-store.js +9 -0
- package/dist/plugins/fork-routing-session-store.d.ts +9 -1
- package/dist/plugins/fork-routing-session-store.js +12 -1
- package/dist/plugins/host-platform.d.ts +2 -0
- package/dist/plugins/host-platform.js +2 -1
- package/dist/plugins/local-checkpoint-store.d.ts +2 -0
- package/dist/plugins/local-checkpoint-store.js +6 -1
- package/dist/plugins/local-session-store.d.ts +1 -1
- package/dist/plugins/local-session-window.d.ts +39 -0
- package/dist/plugins/local-session-window.js +54 -0
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +3 -2
- package/dist/plugins/mailbox-store-sql.d.ts +30 -48
- package/dist/plugins/mailbox-store-sql.js +77 -52
- package/dist/plugins/memory-embedder-fingerprint.js +2 -1
- package/dist/plugins/memory-run-store.d.ts +5 -13
- package/dist/plugins/memory-run-store.js +8 -0
- package/dist/plugins/one-time-migrations.d.ts +110 -5
- package/dist/plugins/one-time-migrations.js +320 -8
- package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
- package/dist/plugins/org-delivery-quota-store.js +228 -0
- package/dist/plugins/pg-pool.js +39 -7
- package/dist/plugins/pg-session-storage.d.ts +17 -4
- package/dist/plugins/pg-session-storage.js +29 -6
- package/dist/plugins/remote-env-adb.d.ts +6 -12
- package/dist/plugins/remote-env-adb.js +12 -2
- package/dist/plugins/remote-env-k8s.d.ts +8 -12
- package/dist/plugins/remote-env-k8s.js +23 -5
- package/dist/plugins/remote-env-local-docker.d.ts +10 -15
- package/dist/plugins/remote-env-local-docker.js +12 -9
- package/dist/plugins/remote-env-ssh.d.ts +10 -16
- package/dist/plugins/remote-env-ssh.js +17 -11
- package/dist/plugins/run-store-sql.d.ts +16 -14
- package/dist/plugins/run-store-sql.js +22 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-user-file.js +2 -2
- package/dist/plugins/session-epoch-anchor.d.ts +32 -6
- package/dist/plugins/session-epoch-anchor.js +2 -3
- package/dist/plugins/session-store.d.ts +18 -8
- package/dist/plugins/session-store.js +15 -4
- package/dist/plugins/sql-driver.d.ts +74 -11
- package/dist/plugins/sql-driver.js +210 -12
- package/dist/plugins/sql-errors.d.ts +23 -0
- package/dist/plugins/sql-errors.js +21 -0
- package/dist/plugins/store-backend.d.ts +36 -1
- package/dist/plugins/store-backend.js +40 -14
- package/dist/plugins/store-contracts.d.ts +42 -0
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/plugins/tidb-pool.d.ts +6 -1
- package/dist/plugins/tidb-pool.js +44 -10
- package/dist/plugins/tidb-session-storage.d.ts +3 -2
- package/dist/plugins/tidb-session-storage.js +2 -2
- package/dist/plugins/tidb-session-store.d.ts +24 -4
- package/dist/plugins/tidb-session-store.js +28 -4
- package/dist/plugins/workflow-journal-store-sql.d.ts +73 -19
- package/dist/plugins/workflow-journal-store-sql.js +19 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +30 -8
- package/dist/plugins/workflow-run-store-sql.js +50 -9
- package/dist/posture-source.d.ts +18 -10
- package/dist/posture-source.js +4 -1
- package/dist/project-identity.d.ts +45 -6
- package/dist/project-identity.js +162 -8
- package/dist/project-layer-key.d.ts +28 -0
- package/dist/project-layer-key.js +29 -0
- package/dist/question.js +3 -2
- package/dist/read-deny-entry.d.ts +25 -0
- package/dist/read-deny-entry.js +12 -0
- package/dist/read-face-posture.d.ts +37 -11
- package/dist/read-face-posture.js +32 -3
- package/dist/request-key-closure.d.ts +7 -0
- package/dist/request-key-closure.js +9 -1
- package/dist/request-value-closure.d.ts +5 -0
- package/dist/request-value-closure.js +11 -3
- package/dist/rule-entry-list.d.ts +26 -2
- package/dist/rule-entry-list.js +72 -12
- package/dist/rules-consent.js +3 -4
- package/dist/run-acceptance.d.ts +19 -3
- package/dist/run-acceptance.js +13 -1
- package/dist/run-local.js +36 -6
- package/dist/runs.d.ts +10 -9
- package/dist/runs.js +21 -3
- package/dist/runtime-caps-resolver.d.ts +16 -6
- package/dist/runtime-caps-resolver.js +18 -5
- package/dist/sandbox-pkg-source.d.ts +19 -1
- package/dist/sandbox-pkg-source.js +29 -7
- package/dist/security.d.ts +87 -9
- package/dist/security.js +52 -9
- package/dist/session-error-wire.d.ts +19 -2
- package/dist/session-error-wire.js +5 -1
- package/dist/session-titled-frame.d.ts +33 -0
- package/dist/session-titled-frame.js +4 -0
- package/dist/session-titler.d.ts +9 -1
- package/dist/session-titler.js +15 -4
- package/dist/spec-fields.js +21 -2
- package/dist/sql-engine-posture.d.ts +3 -1
- package/dist/task-cwd.d.ts +1 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-mcp.js +7 -66
- package/dist/task-settings.d.ts +89 -88
- package/dist/task-settings.js +71 -63
- package/dist/text-bidi.d.ts +0 -4
- package/dist/text-bidi.js +1 -3
- package/dist/tighten-only-seats.d.ts +150 -0
- package/dist/tighten-only-seats.js +196 -0
- package/dist/tool-approval.d.ts +303 -87
- package/dist/tool-approval.js +623 -239
- package/dist/trace/core-keyset-guard.d.ts +104 -22
- package/dist/trace/exit-seat.d.ts +28 -0
- package/dist/trace/exit-seat.js +76 -0
- package/dist/trace/ledger-events.d.ts +3 -2
- package/dist/trace/project.d.ts +80 -12
- package/dist/trace/project.js +41 -28
- package/dist/trace/projection-table.d.ts +100 -0
- package/dist/trace/projection-table.js +130 -0
- package/dist/trace/redact.d.ts +54 -0
- package/dist/trace/redact.js +778 -152
- package/dist/trace/self-minted-wire.d.ts +345 -1
- package/dist/trace/self-minted-wire.js +117 -1
- package/dist/trace/wire-projection-faces.d.ts +298 -2
- package/dist/trace/wire-projection-faces.js +154 -13
- package/dist/url-refusal.js +2 -0
- package/dist/web-admin-client/apply-effective.d.ts +12 -0
- package/dist/web-admin-client/apply-effective.js +56 -5
- package/dist/web-admin-client/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/read-face.js +5 -9
- package/dist/web-admin-client/skills-mcp.js +4 -5
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -9
- package/package.json +7 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** 拒的机器码:起服拒启的 fatal 标题(`[config.dir_untrusted]`)与每拍那一行的句首同用。 */
|
|
2
|
+
export declare const CONFIG_DIR_UNTRUSTED_CODE = "config.dir_untrusted";
|
|
3
|
+
/** 属主声明的 env 名(登记 `config-catalog.ts`)。 */
|
|
4
|
+
export declare const CONFIG_LOCAL_OWNER_UID_ENV = "CONFIG_LOCAL_OWNER_UID";
|
|
5
|
+
/** 一份配置文档的字节上界(只在这里定)。挡的是误放的巨型文件 / 误链到日志;技能正文内联在 `skills.json` 里,所以给得宽。 */
|
|
6
|
+
export declare const CONFIG_DOCUMENT_MAX_BYTES: number;
|
|
7
|
+
/** 走一条路径最多跟几跳符号链接(与 Linux `MAXSYMLINKS` 同数)。 */
|
|
8
|
+
export declare const CONFIG_DIR_LINK_HOPS_MAX = 40;
|
|
9
|
+
/** 一条「别人改得动」的发现(闭集;句子见 {@link buildConfigDirUntrustedSentence})。`path` 是物理路径(链接已跟)。 */
|
|
10
|
+
export type ConfigDirUntrustedFinding = {
|
|
11
|
+
readonly kind: "owner";
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly uid: number;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "writable";
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly mode: number;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "not-regular";
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly type: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "too-large";
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly bytes: number;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "link-loop";
|
|
28
|
+
readonly path: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "unreadable";
|
|
31
|
+
readonly path: string;
|
|
32
|
+
readonly errno: string;
|
|
33
|
+
}
|
|
34
|
+
/** 读者会读的这一项不存在,而它所在的目录是开放的粘滞目录 —— 别的用户能在判与读之间把它建出来(缺席没有约束力)。 */
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "creatable";
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly parent: string;
|
|
39
|
+
readonly mode: number;
|
|
40
|
+
}
|
|
41
|
+
/** 走路径找不到、读者却打得开:链接读出的不是路径(procfs 的 `pipe:[…]` / `net:[…]` 一类内核对象)—— 判不了它的属主与模式。 */
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "unresolvable";
|
|
44
|
+
readonly path: string;
|
|
45
|
+
}
|
|
46
|
+
/** 只读挂载的 kubelet ConfigMap 卷 `volume`(卷根模式 `mode`,恒组 / 他人可写)的来源在本命名空间里经可写挂载 `mountPoint` 露在 `path`(见 {@link KubeletVolume.writableExposures}):
|
|
47
|
+
* 走得到 `path` 的人能在判与读之间换掉 `..data`。 */
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: "exposed";
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly mountPoint: string;
|
|
52
|
+
readonly volume: string;
|
|
53
|
+
readonly mode: number;
|
|
54
|
+
};
|
|
55
|
+
/** 判官看的宿主事实(测试注入;生产 = 本进程)。 */
|
|
56
|
+
export interface ConfigDirTrustHost {
|
|
57
|
+
readonly platform: NodeJS.Platform;
|
|
58
|
+
readonly euid: number | undefined;
|
|
59
|
+
/** 本进程的挂载表(`/proc/self/mountinfo` 全文;解读见 {@link kubeletVolumeIn})。缺席 = 本进程实测(Linux 读 procfs;别的平台没有 kubelet 卷,答空表)。只在认 kubelet 卷根时读。 */
|
|
60
|
+
readonly mountinfo?: () => Promise<string>;
|
|
61
|
+
}
|
|
62
|
+
export interface ConfigDirTrustInput {
|
|
63
|
+
/** 本地配置根(持有 `config.d`;`configLocalRoot` 的那一个值)。相对路径按进程 cwd 解,与读者打开文件时同一个解法。 */
|
|
64
|
+
readonly root: string;
|
|
65
|
+
/** `CONFIG_LOCAL_OWNER_UID`(配置席 `configLocalOwnerUid`);缺席 = 只信本进程 euid 与 root。 */
|
|
66
|
+
readonly declaredOwnerUid: number | undefined;
|
|
67
|
+
/** 缺省 = 本进程。 */
|
|
68
|
+
readonly host?: ConfigDirTrustHost;
|
|
69
|
+
}
|
|
70
|
+
/** 来源在本命名空间里的一处可写露出:`path` = 那条路径,`mountPoint` = 露出它的那只可写挂载的挂载点。 */
|
|
71
|
+
export interface KubeletVolumeExposure {
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly mountPoint: string;
|
|
74
|
+
}
|
|
75
|
+
/** 一只只读挂载的 kubelet ConfigMap 卷(见 {@link kubeletVolumeIn})。 */
|
|
76
|
+
export interface KubeletVolume {
|
|
77
|
+
/** 卷在源文件系统里的目录(mountinfo 第 4 格,如 `/var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~configmap/<卷>`)。 */
|
|
78
|
+
readonly source: string;
|
|
79
|
+
/**
|
|
80
|
+
* 本命名空间里经**可写**挂载露出这个来源的每一处:同一文件系统(设备号同)、每挂载选项不含 `ro`、挂载根是来源本身或它的祖先 —— 按挂载点映射成一条路径,
|
|
81
|
+
* **且走那条路径看见的正是这只挂载**({@link visibleMountOf}):被只读挂载盖住(那条路径上只读)、被别的文件系统或映射别处的挂载盖住(那条路径上看见的不是来源)⇒ 不是露出;
|
|
82
|
+
* 判不出谁可见(影子挂载)⇒ 算露出(判不出就不认)。
|
|
83
|
+
*/
|
|
84
|
+
readonly writableExposures: readonly KubeletVolumeExposure[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 🔴 **`dir` 是不是一只只读挂载的 kubelet ConfigMap 卷的挂载点**,是则交回它的来源与来源在本命名空间里的可写露出(纯判:读的是 `/proc/self/mountinfo` 的全文)。
|
|
88
|
+
* 先找**走路径看见的那一只挂载**({@link visibleMountOf}:叠在祖先上的挂载会把卷根整个盖住,codex 验证轮 V1 / 二),它的挂载点必须恰是 `dir`,且两件同时成立:
|
|
89
|
+
* 每挂载选项含 `ro`;它在源文件系统里的根路径是 kubelet configMap 卷目录的整形({@link KUBELET_VOLUME_ROOT})。
|
|
90
|
+
* 路径形只认出「这是 kubelet 卷」(任何能写某个目录的人都造得出这个形,修复验证二):来源能不能被人改,看 `writableExposures`(见那一格的可见性律)。
|
|
91
|
+
* 内存介质的卷(Secret / projected / downwardAPI)在 mountinfo 里根路径是一只 tmpfs 的 `/`,认不出 ⇒ 不认(照拒,出路 `subPath` / 烤进镜像)。
|
|
92
|
+
*/
|
|
93
|
+
export declare function kubeletVolumeIn(mountinfo: string, dir: string): KubeletVolume | undefined;
|
|
94
|
+
/** `<root>/config.d` 的绝对路径(相对根按 cwd 解)。 */
|
|
95
|
+
export declare function localConfigDirOf(root: string): string;
|
|
96
|
+
/** 判一次。`undefined` = 读者可以读(或没有可读的);否则那一条发现。 */
|
|
97
|
+
export declare function judgeLocalConfigDir(input: ConfigDirTrustInput): Promise<ConfigDirUntrustedFinding | undefined>;
|
|
98
|
+
/** 一条发现的句子(纯字符串 ⇒ `build*`)。头半句说为什么要判、谁可以改;后半句点名路径、实测值与改法。 */
|
|
99
|
+
export declare function buildConfigDirUntrustedSentence(f: ConfigDirUntrustedFinding, ctx: {
|
|
100
|
+
readonly configDir: string;
|
|
101
|
+
readonly euid: number | undefined;
|
|
102
|
+
readonly declaredOwnerUid: number | undefined;
|
|
103
|
+
}): string;
|
|
104
|
+
/** 判出一条发现时抛的错:`code` = {@link CONFIG_DIR_UNTRUSTED_CODE},`message` = 那一句,`finding` = 结构。 */
|
|
105
|
+
export declare class ConfigDirUntrustedError extends Error {
|
|
106
|
+
readonly code = "config.dir_untrusted";
|
|
107
|
+
readonly finding: ConfigDirUntrustedFinding;
|
|
108
|
+
constructor(sentence: string, finding: ConfigDirUntrustedFinding);
|
|
109
|
+
}
|
|
110
|
+
/** 两个调用点的口:判,有发现就抛 {@link ConfigDirUntrustedError}。 */
|
|
111
|
+
export declare function assertLocalConfigDirTrusted(input: ConfigDirTrustInput): Promise<void>;
|
|
112
|
+
/** `CONFIG_LOCAL_OWNER_UID` 的解析(`loadConfig` 调;无条件解析 —— 形错的值不许躺在一台今天不读 `config.d` 的机器上等哪天生效)。缺席 ⇒ `undefined`;形错 ⇒ 抛 = 拒启。 */
|
|
113
|
+
export declare function parseConfigLocalOwnerUid(raw: string | undefined): number | undefined;
|
|
114
|
+
//# sourceMappingURL=config-dir-trust.d.ts.map
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, readlink, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { LOCAL_READER_DOCUMENTS } from "./config-d-migration.js";
|
|
4
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
5
|
+
import { HOST_PLATFORM } from "./plugins/host-platform.js";
|
|
6
|
+
export const CONFIG_DIR_UNTRUSTED_CODE = "config.dir_untrusted";
|
|
7
|
+
export const CONFIG_LOCAL_OWNER_UID_ENV = "CONFIG_LOCAL_OWNER_UID";
|
|
8
|
+
export const CONFIG_DOCUMENT_MAX_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
export const CONFIG_DIR_LINK_HOPS_MAX = 40;
|
|
10
|
+
const UID_MAX = 4294967294;
|
|
11
|
+
const WIN32_UNVERIFIED_TAG = "server.config-dir-trust.win32-owner-mode-unverifiable";
|
|
12
|
+
const errnoOf = (e) => (e instanceof Error && "code" in e && typeof e.code === "string" ? e.code : undefined);
|
|
13
|
+
const KUBELET_VOLUME_ROOT = /\/pods\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/volumes\/kubernetes\.io~configmap\/[^/]+$/;
|
|
14
|
+
const coversPath = (ancestor, p) => ancestor === p || ancestor === "/" || p.startsWith(`${ancestor}/`);
|
|
15
|
+
const unescapeMountinfoPath = (field) => field.replace(/\\([0-7]{3})/g, (_m, oct) => String.fromCharCode(Number.parseInt(oct, 8)));
|
|
16
|
+
function visibleMountOf(rows, dir) {
|
|
17
|
+
const ids = new Set(rows.map((r) => r.id));
|
|
18
|
+
let level = rows.filter((r) => !ids.has(r.parent) || r.parent === r.id);
|
|
19
|
+
let visible;
|
|
20
|
+
for (let depth = 0; depth <= rows.length; depth++) {
|
|
21
|
+
const covering = level.filter((r) => coversPath(r.mountPoint, dir));
|
|
22
|
+
if (covering.length === 0)
|
|
23
|
+
break;
|
|
24
|
+
const shortest = Math.min(...covering.map((r) => r.mountPoint.length));
|
|
25
|
+
const first = covering.filter((r) => r.mountPoint.length === shortest);
|
|
26
|
+
if (first.length !== 1)
|
|
27
|
+
return "ambiguous";
|
|
28
|
+
const next = first[0];
|
|
29
|
+
visible = next;
|
|
30
|
+
level = rows.filter((r) => r.parent === next.id && r.id !== next.id);
|
|
31
|
+
}
|
|
32
|
+
return visible;
|
|
33
|
+
}
|
|
34
|
+
export function kubeletVolumeIn(mountinfo, dir) {
|
|
35
|
+
const rows = [];
|
|
36
|
+
for (const line of mountinfo.split("\n")) {
|
|
37
|
+
const f = line.split(" ");
|
|
38
|
+
const [id, parent, dev, root, mountPoint, options] = f;
|
|
39
|
+
if (f.indexOf("-", 6) === -1 || id === undefined || parent === undefined || dev === undefined || root === undefined || mountPoint === undefined || options === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
rows.push({ id, parent, dev, root: unescapeMountinfoPath(root), mountPoint: unescapeMountinfoPath(mountPoint), options: options.split(",") });
|
|
42
|
+
}
|
|
43
|
+
const visible = visibleMountOf(rows, dir);
|
|
44
|
+
if (typeof visible !== "object" || visible.mountPoint !== dir || !visible.options.includes("ro") || !KUBELET_VOLUME_ROOT.test(visible.root))
|
|
45
|
+
return undefined;
|
|
46
|
+
const source = visible.root;
|
|
47
|
+
const writableExposures = [];
|
|
48
|
+
for (const r of rows) {
|
|
49
|
+
if (r.dev !== visible.dev || r.options.includes("ro") || !coversPath(r.root, source))
|
|
50
|
+
continue;
|
|
51
|
+
const exposed = path.posix.join(r.mountPoint, r.root === "/" ? source : source.slice(r.root.length));
|
|
52
|
+
const seen = visibleMountOf(rows, exposed);
|
|
53
|
+
if (typeof seen === "object" && seen !== r)
|
|
54
|
+
continue;
|
|
55
|
+
if (!writableExposures.some((e) => e.path === exposed))
|
|
56
|
+
writableExposures.push({ path: exposed, mountPoint: r.mountPoint });
|
|
57
|
+
}
|
|
58
|
+
return { source, writableExposures };
|
|
59
|
+
}
|
|
60
|
+
async function processMountinfo() {
|
|
61
|
+
return HOST_PLATFORM === "linux" ? readFile("/proc/self/mountinfo", "utf8") : "";
|
|
62
|
+
}
|
|
63
|
+
const processHost = () => ({ platform: HOST_PLATFORM, euid: process.geteuid?.(), mountinfo: processMountinfo });
|
|
64
|
+
const UNREACHABLE = new Set(["ENOENT", "ENOTDIR"]);
|
|
65
|
+
const NOT_REACHED = { reached: false };
|
|
66
|
+
function typeOf(st) {
|
|
67
|
+
if (st.isDirectory())
|
|
68
|
+
return "directory";
|
|
69
|
+
if (st.isFIFO())
|
|
70
|
+
return "fifo";
|
|
71
|
+
if (st.isSocket())
|
|
72
|
+
return "socket";
|
|
73
|
+
if (st.isCharacterDevice())
|
|
74
|
+
return "character device";
|
|
75
|
+
if (st.isBlockDevice())
|
|
76
|
+
return "block device";
|
|
77
|
+
return "non-regular file";
|
|
78
|
+
}
|
|
79
|
+
function componentVerdict(p, st, owners) {
|
|
80
|
+
if (!owners.has(st.uid))
|
|
81
|
+
return { kind: "owner", path: p, uid: st.uid };
|
|
82
|
+
if (st.isSymbolicLink() || (st.mode & 0o022) === 0)
|
|
83
|
+
return "closed";
|
|
84
|
+
if (st.isDirectory() && (st.mode & 0o1000) !== 0)
|
|
85
|
+
return "open";
|
|
86
|
+
return { kind: "writable", path: p, mode: st.mode & 0o7777 };
|
|
87
|
+
}
|
|
88
|
+
const KUBELET_NODE_SIDE_TAG = "server.config-dir-trust.kubelet-volume-node-side-unverifiable";
|
|
89
|
+
const KUBELET_TIMESTAMP_DIR = /^\.\.\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}\.\d+$/;
|
|
90
|
+
const KUBELET_DATA_LINKS = new Set(["..data", "..data_tmp"]);
|
|
91
|
+
async function projectedVolumeRoot(dir, mode, ctx) {
|
|
92
|
+
const listing = await readdir(dir).then((names) => ({ names }), (error) => ({ error }));
|
|
93
|
+
if ("error" in listing)
|
|
94
|
+
return false;
|
|
95
|
+
const names = listing.names;
|
|
96
|
+
if (!names.includes("..data"))
|
|
97
|
+
return false;
|
|
98
|
+
let at = dir;
|
|
99
|
+
let volume;
|
|
100
|
+
try {
|
|
101
|
+
const stampDirs = new Set();
|
|
102
|
+
const linkTargets = [];
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
at = path.posix.join(dir, name);
|
|
105
|
+
const st = await lstat(at);
|
|
106
|
+
if (!ctx.owners.has(st.uid))
|
|
107
|
+
return false;
|
|
108
|
+
if (KUBELET_DATA_LINKS.has(name)) {
|
|
109
|
+
if (!st.isSymbolicLink())
|
|
110
|
+
return false;
|
|
111
|
+
linkTargets.push(await readlink(at));
|
|
112
|
+
}
|
|
113
|
+
else if (KUBELET_TIMESTAMP_DIR.test(name)) {
|
|
114
|
+
if (!st.isDirectory() || componentVerdict(at, st, ctx.owners) !== "closed")
|
|
115
|
+
return false;
|
|
116
|
+
stampDirs.add(name);
|
|
117
|
+
}
|
|
118
|
+
else if (name.startsWith("..") || !st.isSymbolicLink() || (await readlink(at)) !== `..data/${name}`) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!linkTargets.every((target) => stampDirs.has(target)))
|
|
123
|
+
return false;
|
|
124
|
+
at = "/proc/self/mountinfo";
|
|
125
|
+
volume = kubeletVolumeIn(await ctx.mountinfo(), dir);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return { kind: "unreadable", path: at, errno: errnoOf(e) ?? String(e) };
|
|
129
|
+
}
|
|
130
|
+
if (volume === undefined)
|
|
131
|
+
return false;
|
|
132
|
+
const exposure = volume.writableExposures[0];
|
|
133
|
+
if (exposure !== undefined)
|
|
134
|
+
return { kind: "exposed", path: exposure.path, mountPoint: exposure.mountPoint, volume: dir, mode };
|
|
135
|
+
recordFailOpen(KUBELET_NODE_SIDE_TAG, `volume=${dir} source=${volume.source}`);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
async function judgeComponent(p, st, ctx) {
|
|
139
|
+
const v = componentVerdict(p, st, ctx.owners);
|
|
140
|
+
if (typeof v === "string" || v.kind !== "writable" || !st.isDirectory())
|
|
141
|
+
return v;
|
|
142
|
+
const projected = await projectedVolumeRoot(p, v.mode, ctx);
|
|
143
|
+
if (projected === true)
|
|
144
|
+
return "closed";
|
|
145
|
+
return projected === false ? v : projected;
|
|
146
|
+
}
|
|
147
|
+
const isStats = (x) => "isDirectory" in x;
|
|
148
|
+
async function lstatStep(p) {
|
|
149
|
+
try {
|
|
150
|
+
return await lstat(p);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
const code = errnoOf(e);
|
|
154
|
+
if (code !== undefined && UNREACHABLE.has(code))
|
|
155
|
+
return NOT_REACHED;
|
|
156
|
+
return { finding: { kind: "unreadable", path: p, errno: code ?? String(e) } };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function walk(from, pending, want, ctx) {
|
|
160
|
+
let cur = from.start;
|
|
161
|
+
let st = from.stats;
|
|
162
|
+
let open = from.open;
|
|
163
|
+
let hops = from.hops;
|
|
164
|
+
const queue = [...pending];
|
|
165
|
+
const enter = async (p) => {
|
|
166
|
+
const got = await lstatStep(p);
|
|
167
|
+
if (!isStats(got))
|
|
168
|
+
return got;
|
|
169
|
+
const v = await judgeComponent(p, got, ctx);
|
|
170
|
+
if (typeof v !== "string")
|
|
171
|
+
return { finding: v };
|
|
172
|
+
cur = p;
|
|
173
|
+
st = got;
|
|
174
|
+
open = v === "open";
|
|
175
|
+
return undefined;
|
|
176
|
+
};
|
|
177
|
+
while (queue.length > 0) {
|
|
178
|
+
const name = queue.shift();
|
|
179
|
+
if (name === "" || name === ".")
|
|
180
|
+
continue;
|
|
181
|
+
if (name === "..") {
|
|
182
|
+
const stop = await enter(path.posix.dirname(cur));
|
|
183
|
+
if (stop !== undefined)
|
|
184
|
+
return stop;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const next = path.posix.join(cur, name);
|
|
188
|
+
const got = await lstatStep(next);
|
|
189
|
+
if (!isStats(got)) {
|
|
190
|
+
if (!("reached" in got) || !open)
|
|
191
|
+
return got;
|
|
192
|
+
return { finding: { kind: "creatable", path: next, parent: cur, mode: st.mode & 0o7777 } };
|
|
193
|
+
}
|
|
194
|
+
const v = await judgeComponent(next, got, ctx);
|
|
195
|
+
if (typeof v !== "string")
|
|
196
|
+
return { finding: v };
|
|
197
|
+
if (got.isSymbolicLink()) {
|
|
198
|
+
if (++hops > CONFIG_DIR_LINK_HOPS_MAX)
|
|
199
|
+
return { finding: { kind: "link-loop", path: next } };
|
|
200
|
+
let target;
|
|
201
|
+
try {
|
|
202
|
+
target = await readlink(next);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
const code = errnoOf(e);
|
|
206
|
+
if (code !== undefined && UNREACHABLE.has(code))
|
|
207
|
+
return NOT_REACHED;
|
|
208
|
+
return { finding: { kind: "unreadable", path: next, errno: code ?? String(e) } };
|
|
209
|
+
}
|
|
210
|
+
queue.unshift(...target.split("/"));
|
|
211
|
+
if (target.startsWith("/")) {
|
|
212
|
+
const stop = await enter("/");
|
|
213
|
+
if (stop !== undefined)
|
|
214
|
+
return stop;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (queue.some((n) => n !== "" && n !== ".") && !got.isDirectory())
|
|
219
|
+
return NOT_REACHED;
|
|
220
|
+
cur = next;
|
|
221
|
+
st = got;
|
|
222
|
+
open = v === "open";
|
|
223
|
+
}
|
|
224
|
+
if (want === "dir")
|
|
225
|
+
return st.isDirectory() ? { reached: true, physical: cur, stats: st, open, hops } : NOT_REACHED;
|
|
226
|
+
if (!st.isFile())
|
|
227
|
+
return { finding: { kind: "not-regular", path: cur, type: typeOf(st) } };
|
|
228
|
+
if (st.size > CONFIG_DOCUMENT_MAX_BYTES)
|
|
229
|
+
return { finding: { kind: "too-large", path: cur, bytes: st.size } };
|
|
230
|
+
return { reached: true, physical: cur, stats: st, open, hops };
|
|
231
|
+
}
|
|
232
|
+
async function confirmUnreached(lexical) {
|
|
233
|
+
try {
|
|
234
|
+
await stat(lexical);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
const code = errnoOf(e);
|
|
238
|
+
if (code !== undefined && UNREACHABLE.has(code))
|
|
239
|
+
return undefined;
|
|
240
|
+
if (code === "ELOOP")
|
|
241
|
+
return { kind: "link-loop", path: lexical };
|
|
242
|
+
return { kind: "unreadable", path: lexical, errno: code ?? String(e) };
|
|
243
|
+
}
|
|
244
|
+
return { kind: "unresolvable", path: lexical };
|
|
245
|
+
}
|
|
246
|
+
const documentFiles = () => LOCAL_READER_DOCUMENTS.map((d) => `${d}.json`);
|
|
247
|
+
async function reachDir(lexical, ctx) {
|
|
248
|
+
const top = await lstatStep("/");
|
|
249
|
+
if (!isStats(top))
|
|
250
|
+
return "finding" in top ? top.finding : undefined;
|
|
251
|
+
const rootVerdict = await judgeComponent("/", top, ctx);
|
|
252
|
+
if (typeof rootVerdict !== "string")
|
|
253
|
+
return rootVerdict;
|
|
254
|
+
const step = await walk({ start: "/", stats: top, open: rootVerdict === "open", hops: 0 }, lexical.split("/"), "dir", ctx);
|
|
255
|
+
if ("finding" in step)
|
|
256
|
+
return step.finding;
|
|
257
|
+
return step.reached ? step : confirmUnreached(lexical);
|
|
258
|
+
}
|
|
259
|
+
async function judgePosix(configDir, ctx) {
|
|
260
|
+
const dir = await reachDir(configDir, ctx);
|
|
261
|
+
if (dir === undefined || "kind" in dir)
|
|
262
|
+
return dir;
|
|
263
|
+
for (const file of documentFiles()) {
|
|
264
|
+
const doc = await walk({ start: dir.physical, stats: dir.stats, open: dir.open, hops: dir.hops }, [file], "file", ctx);
|
|
265
|
+
if ("finding" in doc)
|
|
266
|
+
return doc.finding;
|
|
267
|
+
if (!doc.reached) {
|
|
268
|
+
const unseen = await confirmUnreached(path.join(configDir, file));
|
|
269
|
+
if (unseen !== undefined)
|
|
270
|
+
return unseen;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
async function judgeWin32(configDir) {
|
|
276
|
+
const resolveStep = async (p) => {
|
|
277
|
+
try {
|
|
278
|
+
return await realpath(p);
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
const code = errnoOf(e);
|
|
282
|
+
if (code !== undefined && UNREACHABLE.has(code))
|
|
283
|
+
return undefined;
|
|
284
|
+
if (code === "ELOOP")
|
|
285
|
+
return { kind: "link-loop", path: p };
|
|
286
|
+
return { kind: "unreadable", path: p, errno: code ?? String(e) };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const dir = await resolveStep(configDir);
|
|
290
|
+
if (dir === undefined)
|
|
291
|
+
return undefined;
|
|
292
|
+
if (typeof dir !== "string")
|
|
293
|
+
return dir;
|
|
294
|
+
if (!(await stat(dir)).isDirectory())
|
|
295
|
+
return undefined;
|
|
296
|
+
for (const file of documentFiles()) {
|
|
297
|
+
const doc = await resolveStep(path.join(dir, file));
|
|
298
|
+
if (doc === undefined)
|
|
299
|
+
continue;
|
|
300
|
+
if (typeof doc !== "string")
|
|
301
|
+
return doc;
|
|
302
|
+
const st = await stat(doc);
|
|
303
|
+
if (!st.isFile())
|
|
304
|
+
return { kind: "not-regular", path: doc, type: typeOf(st) };
|
|
305
|
+
if (st.size > CONFIG_DOCUMENT_MAX_BYTES)
|
|
306
|
+
return { kind: "too-large", path: doc, bytes: st.size };
|
|
307
|
+
}
|
|
308
|
+
recordFailOpen(WIN32_UNVERIFIED_TAG, `config.d=${dir}`);
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
export function localConfigDirOf(root) {
|
|
312
|
+
return path.join(path.resolve(root), "config.d");
|
|
313
|
+
}
|
|
314
|
+
export async function judgeLocalConfigDir(input) {
|
|
315
|
+
const host = input.host ?? processHost();
|
|
316
|
+
const configDir = localConfigDirOf(input.root);
|
|
317
|
+
if (host.platform === "win32")
|
|
318
|
+
return judgeWin32(configDir);
|
|
319
|
+
const owners = new Set([0]);
|
|
320
|
+
if (host.euid !== undefined)
|
|
321
|
+
owners.add(host.euid);
|
|
322
|
+
if (input.declaredOwnerUid !== undefined)
|
|
323
|
+
owners.add(input.declaredOwnerUid);
|
|
324
|
+
return judgePosix(configDir, { owners, mountinfo: host.mountinfo ?? processMountinfo });
|
|
325
|
+
}
|
|
326
|
+
const octal = (mode) => `0${(mode & 0o7777).toString(8).padStart(3, "0")}`;
|
|
327
|
+
export function buildConfigDirUntrustedSentence(f, ctx) {
|
|
328
|
+
const self = ctx.euid === undefined ? "this server's user" : `this server's user (uid ${ctx.euid})`;
|
|
329
|
+
const declared = ctx.declaredOwnerUid === undefined ? "" : ` or the declared owner ${CONFIG_LOCAL_OWNER_UID_ENV}=${ctx.declaredOwnerUid}`;
|
|
330
|
+
const head = `${CONFIG_DIR_UNTRUSTED_CODE}: refusing to read the local config source ${ctx.configDir} — its documents decide which commands this server starts (mcp.json) ` +
|
|
331
|
+
`and where it sends credentials (models.json), so only ${self}, root${declared} may be able to change them, but`;
|
|
332
|
+
switch (f.kind) {
|
|
333
|
+
case "owner":
|
|
334
|
+
return (`${head} ${f.path} is owned by uid ${f.uid}${ctx.declaredOwnerUid === undefined ? ` (${CONFIG_LOCAL_OWNER_UID_ENV} is not set)` : ""}. ` +
|
|
335
|
+
`Fix: chown ${ctx.euid ?? "<this server's uid>"} ${f.path}, or declare that owner with ${CONFIG_LOCAL_OWNER_UID_ENV}=${f.uid}.`);
|
|
336
|
+
case "writable": {
|
|
337
|
+
const who = (f.mode & 0o020) !== 0 && (f.mode & 0o002) !== 0 ? "group- and other" : (f.mode & 0o020) !== 0 ? "group" : "other";
|
|
338
|
+
return `${head} ${f.path} is ${who}-writable (mode ${octal(f.mode)}). Fix: chmod go-w ${f.path}.`;
|
|
339
|
+
}
|
|
340
|
+
case "not-regular":
|
|
341
|
+
return `${head} ${f.path} is a ${f.type}, not a regular file. Fix: replace it with a regular file, or remove it.`;
|
|
342
|
+
case "too-large":
|
|
343
|
+
return `${head} ${f.path} is ${f.bytes} bytes, over the ${CONFIG_DOCUMENT_MAX_BYTES}-byte cap for one config document. Fix: shrink it.`;
|
|
344
|
+
case "link-loop":
|
|
345
|
+
return `${head} ${f.path} leads through more than ${CONFIG_DIR_LINK_HOPS_MAX} symbolic links. Fix: point the link at the file directly.`;
|
|
346
|
+
case "unreadable":
|
|
347
|
+
return `${head} ${f.path} cannot be inspected (${f.errno}), so its owner and mode cannot be checked. Fix: give this server's user search access to every directory on the path.`;
|
|
348
|
+
case "creatable":
|
|
349
|
+
return (`${head} ${f.path} does not exist and ${f.parent} (mode ${octal(f.mode)}) lets other users create it between this check and the read. ` +
|
|
350
|
+
`Fix: create ${f.path} as this server's user, or move the config root (CONFIG_LOCAL_DIR) under a directory only this server's user or root can write.`);
|
|
351
|
+
case "unresolvable":
|
|
352
|
+
return `${head} ${f.path} is a symbolic link that does not resolve to a path (it points at a kernel object such as a /proc descriptor), so its owner and mode cannot be checked. Fix: point it at a regular file.`;
|
|
353
|
+
case "exposed":
|
|
354
|
+
return (`${head} ${f.volume} is a read-only Kubernetes ConfigMap volume whose source is also reachable through the writable mount at ${f.mountPoint}, as ${f.path} ` +
|
|
355
|
+
`(mode ${octal(f.mode)}), so whoever can reach that path can replace these documents between this check and the read. ` +
|
|
356
|
+
`Fix: remove the writable mount at ${f.mountPoint} — do not mount the node's root, its kubelet directory or any other ancestor of that directory writable into this pod; mount the config with subPath, or do not mount that ancestor at all. Do not relax permissions on the node.`);
|
|
357
|
+
default: {
|
|
358
|
+
const unhandled = f;
|
|
359
|
+
throw new Error(`buildConfigDirUntrustedSentence: unhandled finding ${JSON.stringify(unhandled)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
export class ConfigDirUntrustedError extends Error {
|
|
364
|
+
code = CONFIG_DIR_UNTRUSTED_CODE;
|
|
365
|
+
finding;
|
|
366
|
+
constructor(sentence, finding) {
|
|
367
|
+
super(sentence);
|
|
368
|
+
this.name = "ConfigDirUntrustedError";
|
|
369
|
+
this.finding = finding;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export async function assertLocalConfigDirTrusted(input) {
|
|
373
|
+
const host = input.host ?? processHost();
|
|
374
|
+
const finding = await judgeLocalConfigDir({ ...input, host });
|
|
375
|
+
if (finding === undefined)
|
|
376
|
+
return;
|
|
377
|
+
throw new ConfigDirUntrustedError(buildConfigDirUntrustedSentence(finding, { configDir: localConfigDirOf(input.root), euid: host.euid, declaredOwnerUid: input.declaredOwnerUid }), finding);
|
|
378
|
+
}
|
|
379
|
+
export function parseConfigLocalOwnerUid(raw) {
|
|
380
|
+
if (raw === undefined)
|
|
381
|
+
return undefined;
|
|
382
|
+
const n = /^(0|[1-9][0-9]{0,9})$/.test(raw) ? Number(raw) : Number.NaN;
|
|
383
|
+
if (Number.isInteger(n) && n <= UID_MAX)
|
|
384
|
+
return n;
|
|
385
|
+
throw new Error(`env ${CONFIG_LOCAL_OWNER_UID_ENV}=${JSON.stringify(raw)} must be a uid — a decimal integer in [0, ${UID_MAX}]: the one extra owner (besides this server's user and root) ` +
|
|
386
|
+
`the local config source's directories and documents may have (a userns-remapped bind mount, an operator-owned config). Unset it to trust only this server's user and root.`);
|
|
387
|
+
}
|
|
388
|
+
//# sourceMappingURL=config-dir-trust.js.map
|
|
@@ -34,9 +34,9 @@ import type { ServiceConfigFlat } from "./config-types.js";
|
|
|
34
34
|
*/
|
|
35
35
|
export declare const LIVENESS_HEARTBEAT_MS = 30000;
|
|
36
36
|
/** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */
|
|
37
|
-
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "multi-tenant-shared-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "model-route-endpoint" | "default-model-resolvable" | "refused-names-not-minted" | "require-execution-lane" | "center-one-identity";
|
|
37
|
+
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "operator-single-user" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "multi-tenant-shared-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "model-route-endpoint" | "default-model-resolvable" | "refused-names-not-minted" | "require-execution-lane" | "center-one-identity" | "workflow-park-decision-domain";
|
|
38
38
|
/** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */
|
|
39
|
-
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "models" | "modelRefusals" | "tiers" | "authBridgeIssuer" | "webAdmin">>;
|
|
39
|
+
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "operatorSingleUser" | "sensitiveWritePatterns" | "leaderEnabled" | "selfOrchestrationEnabled" | "workflowRunStoreBackend" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "models" | "modelRefusals" | "tiers" | "authBridgeIssuer" | "webAdmin">>;
|
|
40
40
|
export interface CrossInvariantInput {
|
|
41
41
|
/** 现役配置(boot 期 = 候选自身)。 */
|
|
42
42
|
readonly live: CrossInvariantView;
|
|
@@ -68,11 +68,17 @@ export interface CrossInvariant {
|
|
|
68
68
|
/** `direct-door-anchors` 的状态臂:半开的直连门(开着却缺锚点)。转换臂(门姿态翻转)只活在热路径,留在条目里。 */
|
|
69
69
|
export declare function halfOpenDirectDoorRefusal(v: Pick<CrossInvariantView, "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals">): string | undefined;
|
|
70
70
|
/**
|
|
71
|
-
* `operator-principals-required`([ref]-① 普查候选 1/2):BFF 多租户 durable 形(DURABLE_APPROVAL ∧ REQUIRE_PRINCIPAL,直连门关)的空 operator
|
|
72
|
-
*
|
|
71
|
+
* `operator-principals-required`([ref]-① 普查候选 1/2):BFF 多租户 durable 形(DURABLE_APPROVAL ∧ REQUIRE_PRINCIPAL,直连门关)的空 operator 名单。
|
|
72
|
+
* 这条不变量生于审批四读面读「空名单 = 人人是」那条读法的年代(那时它防的是跨租户读待批队列);S-1050 删了那条读法 —— 空名单在每一面都是「没人是」,
|
|
73
|
+
* 不再外泄任何东西 —— 剩下的事实是「这台多租 worker 的审批队列没有人能当运维」。本版只把句子改真、不改拒启集;删不删这一条是另一次表态。
|
|
73
74
|
*/
|
|
74
75
|
export declare function operatorPrincipalsRequiredRefusal(v: Pick<CrossInvariantView, "durableApproval" | "requirePrincipal" | "operatorPrincipals">): string | undefined;
|
|
75
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* `operator-single-user`(S-1050,[ref] ① 裁 A):「谁是 operator」一个事实一个来源。`OPERATOR_SINGLE_USER=true`(单用户形的每个调用方都是运维)
|
|
78
|
+
* 只在单用户形、名单为空时成立:多租形没有「单一用户」(声明会把每个租户都变成运维);名单非空时两个来源答同一个问题。两形都拒,句子点名两根旋钮与出路。
|
|
79
|
+
*/
|
|
80
|
+
export declare function operatorSingleUserRefusal(v: Pick<CrossInvariantView, "operatorSingleUser" | "requirePrincipal" | "operatorPrincipals">): string | undefined;
|
|
81
|
+
/** `bake-operator-principals`(IMAGE-API-DESIGN §P2.4a):bake 门是 build-host 无限制执行面,要**具名**运维 —— 空名单下要么没人用得了,要么(单用户声明)人人用得了。 */
|
|
76
82
|
export declare function bakeOperatorPrincipalsRefusal(v: Pick<CrossInvariantView, "imageBakes" | "operatorPrincipals">): string | undefined;
|
|
77
83
|
/**
|
|
78
84
|
* `bake-runner-credential`(IMAGE-API-DESIGN §P2.12 / H1):bake-runner 的身份由它的 bearer 凭据**派生**(token-derived `source`),不是自称的头 ——
|
|
@@ -84,6 +90,13 @@ export declare function bakeRunnerCredentialRefusal(v: Pick<CrossInvariantView,
|
|
|
84
90
|
* 「多副本」在 config 层判不了(副本数不是本进程的知识),可判的**代理**就是 durable 前提本身。
|
|
85
91
|
*/
|
|
86
92
|
export declare function leaderDurableStoreRefusal(v: Pick<CrossInvariantView, "leaderEnabled" | "dbBackend">): string | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* `workflow-park-decision-domain`(7.133.0)—— workflow 子代审批决定行的店必须与会话认领同域。决定行在 run 店的会话认领栅栏里写(SQL 后端一个事务、同一只池),
|
|
95
|
+
* 而库里的决定行随库里的 workflow 记录收割 ⇒ SQL 后端上 workflow 记录也必须在库里(`WORKFLOW_RUN_STORE=auto`)。`file` / `memory` 把记录放在库外,没有一只决定店两头都满足。
|
|
96
|
+
* 判的是**这份配置会不会装出 SQL 店**(坐标铸出来了没有,与 `device-durable-store` 同读法),不是 `dbBackend` 标签。**配置组合的拒启** —— 判在第 1 段(loadConfig),
|
|
97
|
+
* 先于第 3 段的一次性迁移:拒的那一次起服没有动过库。`createHttpServer` 里的 `parkDecisionStoreRefusal` 判的是另一件事(嵌入方递进来的决定店绑没绑在这只 run 店上,按身份比)。
|
|
98
|
+
*/
|
|
99
|
+
export declare function workflowParkDecisionDomainRefusal(v: Pick<CrossInvariantView, "selfOrchestrationEnabled" | "workflowRunStoreBackend" | "dbBackend" | "tidb" | "pg">): string | undefined;
|
|
87
100
|
/**
|
|
88
101
|
* `center-one-identity`(7.124.0)—— **中心只有一个身份**。引擎接的 web-admin 由三只旋钮描述:基址(`WEB_ADMIN_ENV.url`,传输地址)、
|
|
89
102
|
* 鉴权桥 issuer(`AUTH_BRIDGE_ISSUER`,缺席 = 基址 —— {@link authBridgeIssuerOf} 一只取值口)、web 基址(`WEB_ADMIN_ENV.webUrl`,上
|
|
@@ -34,19 +34,28 @@ export function halfOpenDirectDoorRefusal(v) {
|
|
|
34
34
|
if (!v.principalJwtAud)
|
|
35
35
|
missing.push("PRINCIPAL_JWT_AUD");
|
|
36
36
|
if (v.operatorPrincipals.length === 0)
|
|
37
|
-
missing.push("OPERATOR_PRINCIPALS (
|
|
37
|
+
missing.push("OPERATOR_PRINCIPALS (an empty list means no operator, and the direct door's operator faces need named operators)");
|
|
38
38
|
if (!v.requirePrincipal)
|
|
39
39
|
missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the self-asserted header governs owner/quota checks)");
|
|
40
40
|
return missing.length > 0 ? `DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)` : undefined;
|
|
41
41
|
}
|
|
42
42
|
export function operatorPrincipalsRequiredRefusal(v) {
|
|
43
43
|
return v.durableApproval === true && v.requirePrincipal && v.operatorPrincipals.length === 0
|
|
44
|
-
? "DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty —
|
|
44
|
+
? "DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — an empty operator list means no operator, so nobody could operate this multi-tenant worker's approval queue (list any owner's pending via GET /v1/approvals & siblings, decide any approval). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)"
|
|
45
45
|
: undefined;
|
|
46
46
|
}
|
|
47
|
+
export function operatorSingleUserRefusal(v) {
|
|
48
|
+
if (v.operatorSingleUser !== true)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (v.requirePrincipal)
|
|
51
|
+
return "OPERATOR_SINGLE_USER=true but REQUIRE_PRINCIPAL=true — the declaration makes every caller an operator, which only fits a single-user deployment; a multi-tenant worker names its operators in OPERATOR_PRINCIPALS. Unset OPERATOR_SINGLE_USER, or unset REQUIRE_PRINCIPAL if this is a single-user deployment";
|
|
52
|
+
if (v.operatorPrincipals.length > 0)
|
|
53
|
+
return "OPERATOR_SINGLE_USER=true but OPERATOR_PRINCIPALS is also set — the two answer the same question (who is an operator) in two ways. Keep one: OPERATOR_SINGLE_USER=true (every caller of this single-user deployment) or OPERATOR_PRINCIPALS (only the named principals)";
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
47
56
|
export function bakeOperatorPrincipalsRefusal(v) {
|
|
48
57
|
return v.imageBakes.enabled && v.operatorPrincipals.length === 0
|
|
49
|
-
? "IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host
|
|
58
|
+
? "IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host and needs named operators: with an empty list nobody can use it, and the single-user operator declaration would open it to every caller (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)"
|
|
50
59
|
: undefined;
|
|
51
60
|
}
|
|
52
61
|
export function bakeRunnerCredentialRefusal(v) {
|
|
@@ -64,6 +73,15 @@ export function leaderDurableStoreRefusal(v) {
|
|
|
64
73
|
"(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)"
|
|
65
74
|
: undefined;
|
|
66
75
|
}
|
|
76
|
+
export function workflowParkDecisionDomainRefusal(v) {
|
|
77
|
+
const sqlStore = (v.dbBackend === "mysql" && v.tidb !== undefined) || (v.dbBackend === "pg" && v.pg !== undefined);
|
|
78
|
+
return sqlStore && v.selfOrchestrationEnabled && v.workflowRunStoreBackend !== "auto"
|
|
79
|
+
? `DB_BACKEND=${v.dbBackend} with self-orchestration on and WORKFLOW_RUN_STORE=${v.workflowRunStoreBackend} is refused before any schema migration runs (the database was not touched) ` +
|
|
80
|
+
"[workflow-park-decision-domain]: a workflow approval decision is written in the same atomic step that checks the delivering run still holds the host session, so on a SQL backend it lives in the database " +
|
|
81
|
+
"with the session claims and is reaped with the workflow records — which this setting keeps off the database. " +
|
|
82
|
+
"Leave WORKFLOW_RUN_STORE=auto (unset it: the workflow records go to the same database), or turn self-orchestration off (SELF_ORCHESTRATION_ENABLED=false)"
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
|
67
85
|
export function centerIdentityRefusal(v) {
|
|
68
86
|
const webBaseUrl = v.webAdmin?.webBaseUrl;
|
|
69
87
|
if (webBaseUrl === undefined)
|
|
@@ -125,6 +143,13 @@ export const CONFIG_CROSS_INVARIANTS = [
|
|
|
125
143
|
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:operator-principals-required" },
|
|
126
144
|
check: ({ next }) => operatorPrincipalsRequiredRefusal(next),
|
|
127
145
|
},
|
|
146
|
+
{
|
|
147
|
+
id: "operator-single-user",
|
|
148
|
+
keys: ["operatorSingleUser", "requirePrincipal", "operatorPrincipals"],
|
|
149
|
+
restartReason: "operator-principals",
|
|
150
|
+
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:operator-single-user" },
|
|
151
|
+
check: ({ next }) => operatorSingleUserRefusal(next),
|
|
152
|
+
},
|
|
128
153
|
{
|
|
129
154
|
id: "bake-operator-principals",
|
|
130
155
|
keys: ["imageBakes", "operatorPrincipals"],
|
|
@@ -183,6 +208,13 @@ export const CONFIG_CROSS_INVARIANTS = [
|
|
|
183
208
|
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:leader-durable-store" },
|
|
184
209
|
check: ({ next }) => leaderDurableStoreRefusal(next),
|
|
185
210
|
},
|
|
211
|
+
{
|
|
212
|
+
id: "workflow-park-decision-domain",
|
|
213
|
+
keys: ["selfOrchestrationEnabled", "workflowRunStoreBackend", "dbBackend", "tidb", "pg"],
|
|
214
|
+
restartReason: "workflow-orchestration",
|
|
215
|
+
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:workflow-park-decision-domain" },
|
|
216
|
+
check: ({ next }) => workflowParkDecisionDomainRefusal(next),
|
|
217
|
+
},
|
|
186
218
|
{
|
|
187
219
|
id: "device-durable-store",
|
|
188
220
|
keys: ["remoteExec", "dbBackend", "tidb", "pg"],
|
|
@@ -127,6 +127,9 @@ export interface ConfigProviderInput {
|
|
|
127
127
|
* `process.cwd()`,而工厂只在 `CONFIG_PROVIDER === "local"` 字面上递它 ⇒ 另一条走到 local 的路上配置源读 cwd、
|
|
128
128
|
* 合成步读 localRoot,两个根分家。一个远程源也收它:`remote` 而无中心 URL 的形照样落回 local。 */
|
|
129
129
|
localDir: string;
|
|
130
|
+
/** S-163:本地配置源的属主声明(`CONFIG_LOCAL_OWNER_UID`,配置席 `configLocalOwnerUid`)—— 每拍读 `config.d` 之前的目录信任判官与起服那一次判同一个值。
|
|
131
|
+
* **必填**(值可以是 `undefined` = 没声明):两个生产构造点都必须递,漏递 = 起服判过、每拍却按「没声明」判 ⇒ 每一拍都拒(tsc 在这里挡)。 */
|
|
132
|
+
localOwnerUid: number | undefined;
|
|
130
133
|
/** S-1556:远程配置源的技能正文盘缓存目录(见 {@link ConfigProvider.skillBodyCacheDir});本地配置源不认它(正文本就在盘上)。缺席 = 不落盘。 */
|
|
131
134
|
skillBodyCacheDir?: string;
|
|
132
135
|
}
|
|
@@ -165,7 +168,9 @@ export declare class LocalConfigProvider implements ConfigProvider {
|
|
|
165
168
|
/** [ref]b 可观测性:config_loaded 日志要能回答「到底读的哪个目录」——复验实录:只设 LOCAL_DATA_ROOT 时
|
|
166
169
|
* config.d 静默读不到且 0 模型无告警(CONFIG_LOCAL_DIR 才是本 lane 的选目录旋钮)。 */
|
|
167
170
|
readonly root: string;
|
|
168
|
-
constructor(root: string, store?: FileConfigStore
|
|
171
|
+
constructor(root: string, store?: FileConfigStore, trust?: {
|
|
172
|
+
readonly declaredOwnerUid: number | undefined;
|
|
173
|
+
});
|
|
169
174
|
fetchEffective(etag: string | undefined): Promise<FetchEffectiveResult>;
|
|
170
175
|
fetchSkillContent(contentHash: string): Promise<string | undefined>;
|
|
171
176
|
}
|