@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -10,29 +10,14 @@ import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
|
10
10
|
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
11
11
|
const MCP_STATUS_TTL_MS = 5_000;
|
|
12
12
|
const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
|
|
13
|
-
// DESIGN-269 件4:A2A 读面的两枚同族时限。**刻意与 MCP 分开命名而不复用同一个常量**:两条腿的物化
|
|
14
|
-
// 成本不同族(MCP 可能 spawn 子进程/开长连;A2A 只是几个 HTTPS 卡请求),将来任一侧要调窗时不该被
|
|
15
|
-
// 另一侧的名字绑架。数值今天相同,语义各自独立。
|
|
16
13
|
const A2A_STATUS_TTL_MS = 5_000;
|
|
17
14
|
const A2A_MATERIALIZE_TIMEOUT_MS = 10_000;
|
|
18
|
-
/** 面板上每个 peer 最多回多少条工具名(codex finding 5)。超出只截**表**,`toolCount` 仍是真数、
|
|
19
|
-
* `toolNamesTruncated:true` 显式说明 —— 消费端读得出「还有更多」,而不是拿到一份看着完整的假名单。 */
|
|
20
15
|
const A2A_PANEL_MAX_TOOL_NAMES = 200;
|
|
21
16
|
export function createSessionsLocal() {
|
|
22
17
|
return {
|
|
23
|
-
// [1196] session-events SSE 活跃连接计数(连接帽谓词;帽=deps.sessionEventsMaxConnections ?? 256)。
|
|
24
18
|
sseConnections: 0,
|
|
25
|
-
// 六轮复审:初始 owner 查探的**存储侧**并发许可——连接坑在 race 超时即还,但底层查询可能仍悬着
|
|
26
|
-
// (驱动级取消不可得);重连风暴下未决查询必须独立有界(64),超限=零存储触碰直接 503。
|
|
27
19
|
sseOwnerLookups: 0,
|
|
28
|
-
// E9 (shell-host contract) — short-TTL single-flight cache for GET /v1/sessions/:id/mcp. materializeMcpTools connects to (or
|
|
29
|
-
// spawns) MCP servers; without this a burst of /mcp polls would re-connect every time (adversarial-review MEDIUM:
|
|
30
|
-
// resource-exhaustion on an unbounded panel-poll path). Keyed by (scenario, principal) so per-user RBAC stays
|
|
31
|
-
// correct; stores the in-flight PROMISE so concurrent first-polls collapse to ONE materialize. Bounded by TTL +
|
|
32
|
-
// a hard size cap (cleared on overflow). The cached value is the PROJECTED panel (no live connections held).
|
|
33
20
|
mcpStatusCache: new Map(),
|
|
34
|
-
// DESIGN-269 件4 —— 同上那套(TTL + single-flight + 硬条数帽 + 按 (scenario, principal) 分键,因为
|
|
35
|
-
// `principalHeader` 注入让不同调用方看到的 peer 授权面可能不同),A2A 半场。
|
|
36
21
|
a2aStatusCache: new Map(),
|
|
37
22
|
};
|
|
38
23
|
}
|
|
@@ -41,22 +26,15 @@ export async function handleSessions(req, res, url, ctx) {
|
|
|
41
26
|
await handleSessionsBody(req, res, url, ctx, miss);
|
|
42
27
|
return !miss.fell;
|
|
43
28
|
}
|
|
44
|
-
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(去缩进两格;sessions 另有一处见文件头说明)。
|
|
45
|
-
* 裸 `return;` 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。 */
|
|
46
29
|
async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
47
30
|
const { deps } = ctx;
|
|
48
31
|
const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
|
|
49
32
|
const sse = ctx.local.sessions;
|
|
50
33
|
const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
|
|
51
|
-
// E11 (shell-host contract): GET /v1/sessions/:id/init — the one-call startup bundle, so the shell renders its
|
|
52
|
-
// banner from ONE request instead of fanning out to /v1/models + /v1/policy + /v1/capabilities. Worker-global
|
|
53
|
-
// config surface (same posture as /v1/policy): principal OPTIONAL, enforced only under REQUIRE_PRINCIPAL.
|
|
54
|
-
// ADDITIVE — `model` + `mode` are the load-bearing banner fields; `tools`/`agents`/`commands`/`cwd` are omitted
|
|
55
|
-
// today (tolerate-absent — the shell falls back to its mock-fill). `cwd` is live-only (E13) and never durable here.
|
|
56
34
|
{
|
|
57
35
|
const im = /^\/v1\/sessions\/([^/]+)\/init$/.exec(url);
|
|
58
36
|
if (im && req.method === "GET") {
|
|
59
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
37
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
60
38
|
if (deps.config.requirePrincipal && !principal) {
|
|
61
39
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
62
40
|
return;
|
|
@@ -69,15 +47,10 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
69
47
|
return;
|
|
70
48
|
}
|
|
71
49
|
}
|
|
72
|
-
// E24 (shell-host contract): GET /v1/sessions/:id/settings — the effective merged settings view (CC /config read
|
|
73
|
-
// side). Worker-global config surface (principal optional unless REQUIRE_PRINCIPAL), same posture as /v1/policy +
|
|
74
|
-
// the E11 init bundle. `effective` = the resolved worker config the run obeys; `applied` echoes the resolved
|
|
75
|
-
// model. The PATCH half (runtime settings write) + per-source layering need a core seam (SERVICE-GAP-DOSSIER E24)
|
|
76
|
-
// — omitted today, so no `settings` write capability is advertised (honest: read route only).
|
|
77
50
|
{
|
|
78
51
|
const stm = /^\/v1\/sessions\/([^/]+)\/settings$/.exec(url);
|
|
79
52
|
if (stm && req.method === "GET") {
|
|
80
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
53
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
81
54
|
if (deps.config.requirePrincipal && !principal) {
|
|
82
55
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
83
56
|
return;
|
|
@@ -95,38 +68,25 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
95
68
|
return;
|
|
96
69
|
}
|
|
97
70
|
}
|
|
98
|
-
// E9 (shell-host contract, core 1.124.0): GET /v1/sessions/:id/mcp — per-server MCP status for the `/mcp` panel.
|
|
99
|
-
// 🔴 core does NOT persist MCP connections (task-scoped), so there is no stored session MCP health. This
|
|
100
|
-
// MATERIALIZES the worker's configured MCP servers ON-DEMAND (a fresh connect → list → dispose, fail-open) and
|
|
101
|
-
// returns the projected statuses with `asOf` = THIS materialization moment. It is materialization-time status,
|
|
102
|
-
// NOT live session health (core §E9) — the shell renders "as of <asOf>". Worker-global config surface
|
|
103
|
-
// (principal-optional like /policy); the caller's gatedPrincipal is threaded for per-user MCP RBAC
|
|
104
|
-
// (principalHeader injection). Uses the DEFAULT scenario's MCP set (per-session scenario tracking is a follow-up).
|
|
105
71
|
{
|
|
106
72
|
const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
|
|
107
73
|
if (mm && req.method === "GET") {
|
|
108
|
-
// F-fix: VERIFIED presence gate (direct-door JWT, not the spoofable header); per-user MCP RBAC already uses gatedPrincipal.
|
|
109
74
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
110
75
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
111
76
|
return;
|
|
112
77
|
}
|
|
113
78
|
const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
|
|
114
79
|
if (!specs || specs.length === 0) {
|
|
115
|
-
sendJson(res, 200, { servers: [], asOf: new Date().toISOString() });
|
|
80
|
+
sendJson(res, 200, { servers: [], asOf: new Date().toISOString() });
|
|
116
81
|
return;
|
|
117
82
|
}
|
|
118
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
83
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
119
84
|
const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
|
|
120
85
|
const cached = mcpStatusCache.get(cacheKey);
|
|
121
86
|
if (!cached || Date.now() - cached.at > MCP_STATUS_TTL_MS) {
|
|
122
87
|
if (mcpStatusCache.size > 1024)
|
|
123
|
-
mcpStatusCache.clear();
|
|
124
|
-
// Single-flight: store the in-flight promise so a burst of polls collapses to ONE materialize.
|
|
88
|
+
mcpStatusCache.clear();
|
|
125
89
|
const panel = (async () => {
|
|
126
|
-
// Bound the connect/list so a hanging MCP server can't pin the request; dispose the late resolver if it
|
|
127
|
-
// eventually completes after the timeout (no connection leak). materializeMcpTools is fail-open per server.
|
|
128
|
-
// #324:第 6 参接撤销台账 —— 展示面与引擎执行面同一权威(被撤服务器不再进面板;core 契约:
|
|
129
|
-
// probe 抛错 fail-open + onProbeFailure 回调,这里缺席=沿 core 的 once-per-materialization notice)。
|
|
130
90
|
const matP = materializeMcpTools(specs, principal, undefined, undefined, undefined, deps.mcpRevocations);
|
|
131
91
|
let timer;
|
|
132
92
|
const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), MCP_MATERIALIZE_TIMEOUT_MS); })]);
|
|
@@ -134,7 +94,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
134
94
|
clearTimeout(timer);
|
|
135
95
|
if (!mat) {
|
|
136
96
|
void matP.then((m) => m?.dispose().catch(() => undefined)).catch(() => undefined);
|
|
137
|
-
return { servers: [], asOf: new Date().toISOString(), degraded: true };
|
|
97
|
+
return { servers: [], asOf: new Date().toISOString(), degraded: true };
|
|
138
98
|
}
|
|
139
99
|
try {
|
|
140
100
|
return {
|
|
@@ -144,8 +104,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
144
104
|
status: s.status,
|
|
145
105
|
...(s.serverInfo ? { serverInfo: s.serverInfo } : {}),
|
|
146
106
|
...(s.toolNames ? { toolNames: s.toolNames } : {}),
|
|
147
|
-
// E9 (review LOW): redact secret-shaped strings + bound length — raw MCP connect errors may carry
|
|
148
|
-
// internal paths/config the shell needn't see (same redaction posture as tool output).
|
|
149
107
|
...(s.error ? { error: redactSecrets(String(s.error)).slice(0, 200) } : {}),
|
|
150
108
|
})),
|
|
151
109
|
};
|
|
@@ -160,17 +118,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
160
118
|
return;
|
|
161
119
|
}
|
|
162
120
|
}
|
|
163
|
-
// [DESIGN-269 车1 件4] GET /v1/sessions/:id/a2a — per-peer A2A status for the shell's agent panel.
|
|
164
|
-
// 逐点对称上面的 /mcp 面(按需物化 → 投影 → TTL single-flight 缓存 → 超时竞速 + 迟到 dispose),
|
|
165
|
-
// 差别只在**投影形状**,而那是被裁定过的(设计稿 v1.2 §2.5,cli [4107] 采纳级意见):
|
|
166
|
-
//
|
|
167
|
-
// 🔴 **不发裸 `status`**。core 的 `A2aPeerStatus.status:"ready"` 的真语义是「本次探测时,卡取到了、
|
|
168
|
-
// skills 铸出来了」——A2A **不持连接**(每次调用都是独立 HTTPS 请求),所以它永远不是「peer 现在活着」。
|
|
169
|
-
// 一个叫 `status:"ready"` 的键发给三个消费端,必有一端渲成「在线」,而那句话我们证不了。所以投影的
|
|
170
|
-
// 键名自己承重语义:`cardFetched: boolean`(卡取到没有)+ `probedAt: number`(**本次**探测时刻)+
|
|
171
|
-
// 可选 `error`。「在线/离线」这个词在本读面结构性不存在,不是靠文档约束不许渲。
|
|
172
|
-
// `asOf` 与 `probedAt` 并存不是冗余:前者是**面板**的物化时刻(ISO,与 /mcp 同形,壳渲 "as of …"),
|
|
173
|
-
// 后者是**每个 peer** 的探测时刻戳(机器可读,消费端算新鲜度用)。
|
|
174
121
|
{
|
|
175
122
|
const am = /^\/v1\/sessions\/([^/]+)\/a2a$/.exec(url);
|
|
176
123
|
if (am && req.method === "GET") {
|
|
@@ -180,19 +127,16 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
180
127
|
}
|
|
181
128
|
const specs = a2aForScenario(deps.config.a2aPeers, deps.config.defaultScenario);
|
|
182
129
|
if (!specs || specs.length === 0) {
|
|
183
|
-
sendJson(res, 200, { peers: [], asOf: new Date().toISOString() });
|
|
130
|
+
sendJson(res, 200, { peers: [], asOf: new Date().toISOString() });
|
|
184
131
|
return;
|
|
185
132
|
}
|
|
186
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
133
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
187
134
|
const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
|
|
188
135
|
const cached = a2aStatusCache.get(cacheKey);
|
|
189
136
|
if (!cached || Date.now() - cached.at > A2A_STATUS_TTL_MS) {
|
|
190
137
|
if (a2aStatusCache.size > 1024)
|
|
191
|
-
a2aStatusCache.clear();
|
|
138
|
+
a2aStatusCache.clear();
|
|
192
139
|
const panel = (async () => {
|
|
193
|
-
// Bound the card fetches so a hanging peer can't pin the request; dispose the late resolver if it
|
|
194
|
-
// completes after the timeout (materializeA2aTools holds no socket, but its dispose IS the lifetime
|
|
195
|
-
// statement — a tool that outlives this probe must fail loudly instead of calling a remote agent).
|
|
196
140
|
const matP = materializeA2aTools(specs, principal);
|
|
197
141
|
let timer;
|
|
198
142
|
const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), A2A_MATERIALIZE_TIMEOUT_MS); })]);
|
|
@@ -201,28 +145,18 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
201
145
|
const probedAt = Date.now();
|
|
202
146
|
if (!mat) {
|
|
203
147
|
void matP.then((m) => m?.dispose().catch(() => undefined)).catch(() => undefined);
|
|
204
|
-
return { peers: [], asOf: new Date(probedAt).toISOString(), degraded: true };
|
|
148
|
+
return { peers: [], asOf: new Date(probedAt).toISOString(), degraded: true };
|
|
205
149
|
}
|
|
206
150
|
try {
|
|
207
151
|
return {
|
|
208
152
|
asOf: new Date(probedAt).toISOString(),
|
|
209
153
|
peers: mat.statuses.map((s) => ({
|
|
210
154
|
name: s.name,
|
|
211
|
-
// 🔴 翻译点(唯一):core 的闭集 status → 承重键名。`"ready"` = 卡取到了 + skills 铸出来了。
|
|
212
155
|
cardFetched: s.status === "ready",
|
|
213
156
|
probedAt,
|
|
214
|
-
// 卡自述的两串是 UNTRUSTED(core 已中性化+限长);端点是协商结果,诊断用。
|
|
215
157
|
...(s.agentName ? { agentName: s.agentName } : {}),
|
|
216
158
|
...(s.agentDescription ? { agentDescription: s.agentDescription } : {}),
|
|
217
|
-
// 🔒 自审第三镜(泄密面):`endpoint` 是**卡自述**的协商结果 —— 远端可控文本,而且 URL 形
|
|
218
|
-
// 天然能带 userinfo(`https://user:pass@host/rpc`,core 的 `new URL()` 原样保留)。core 对
|
|
219
|
-
// name/description 有中性化+限长,对 endpoint 只验协议(http/https),所以脱敏与限长是**本
|
|
220
|
-
// 读面自己的活**(投影是它的输出,不是 core 的)。2048 = 通用 URL 上限量级,越界的是病态值。
|
|
221
159
|
...(s.endpoint ? { endpoint: redactSecrets(String(s.endpoint)).slice(0, 2048) } : {}),
|
|
222
|
-
// 🔴 codex 对抗复审 finding 5(medium,验真后采纳):技能**条数**是远端可控且无界的
|
|
223
|
-
// (core 限单个名字的长度,不限卡里的条数;卡体上限 8MiB),而这条只读 GET 按
|
|
224
|
-
// (scenario, principal) 分键缓存 ⇒ 多身份可放大。发**有界表 + 真计数 + 显式旗**,
|
|
225
|
-
// 而不是静默截断:一个会撒谎的读面比一个大的读面更贵。
|
|
226
160
|
...(s.toolNames
|
|
227
161
|
? {
|
|
228
162
|
toolCount: s.toolNames.length,
|
|
@@ -230,8 +164,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
230
164
|
...(s.toolNames.length > A2A_PANEL_MAX_TOOL_NAMES ? { toolNamesTruncated: true } : {}),
|
|
231
165
|
}
|
|
232
166
|
: {}),
|
|
233
|
-
// 与 /mcp 同规:原始错误可能带内部路径/凭据形 —— 脱敏 + 截断。
|
|
234
|
-
// 🔒 `headers`/`principalHeader` 在本投影里**根本没有出口**(白名单式铸键,不是删黑名单)。
|
|
235
167
|
...(s.error ? { error: redactSecrets(String(s.error)).slice(0, 200) } : {}),
|
|
236
168
|
})),
|
|
237
169
|
};
|
|
@@ -246,16 +178,9 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
246
178
|
return;
|
|
247
179
|
}
|
|
248
180
|
}
|
|
249
|
-
// [1075]② 会话追随轻探针:GET /v1/sessions/:id/head → { leafId }(+ ETag/304)。leafId=会话树叶
|
|
250
|
-
// 指针,任何 append 都变=天然版本号——web 空闲期低成本轮询(If-None-Match 命中=304 零 body),
|
|
251
|
-
// 变了才拉 tail 增量。刻意不做 session 级 SSE(真订阅需跨副本通知面,写可发生在任意副本而流挂在
|
|
252
|
-
// 某一副本——设计件另立;探针轮询 1-2s 一次的 304 成本≈零,先解「必须刷新」的主诉)。
|
|
253
|
-
// 顶层路由(只依赖 sessionStorage,不依赖 sessionAudit——audit 缺席的部署探针照常)。
|
|
254
181
|
{
|
|
255
182
|
const hm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/head$/.exec(url) : null;
|
|
256
183
|
if (hm) {
|
|
257
|
-
// 二轮复审([1196] S1 邻案连修):方法必须 bind 宿主——SESSION_CACHE_TTL_SEC=0 时 sessionStorage
|
|
258
|
-
// 是裸 PG/TiDB 类实例,getLeafId 内用 this.pool,裸引用调用=TypeError 500。
|
|
259
184
|
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
|
260
185
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
261
186
|
if (!getLeafId || !ownerOf) {
|
|
@@ -263,8 +188,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
263
188
|
return;
|
|
264
189
|
}
|
|
265
190
|
const headSession = decodeURIComponent(hm[1]);
|
|
266
|
-
// 七轮复审:store 有 getHead 时用单行原子快照(owner 鉴权与 leafId 同拍——两独立读在
|
|
267
|
-
// delete/reclaim 缝里可鉴旧主发新叶);无 getHead 的旧 store 保持两读旧形(退化面照旧)。
|
|
268
191
|
const getHead = deps.sessionStorage?.getHead?.bind(deps.sessionStorage);
|
|
269
192
|
let owner;
|
|
270
193
|
let leafId;
|
|
@@ -299,16 +222,9 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
299
222
|
return;
|
|
300
223
|
}
|
|
301
224
|
}
|
|
302
|
-
// [1196] session 级 SSE 真订阅:GET /v1/sessions/:id/events —— `event: head` 推送(latest-wins),
|
|
303
|
-
// `?lastLeafId=` 为游标(等价 If-None-Match:相同则无初始帧),25s 心跳注释行。鉴权/存储门与 /head
|
|
304
|
-
// 完全同门;连接帽超限=503+Retry-After(消费方回落 /head 轮询,capabilities.sessionEvents 广告)。
|
|
305
225
|
{
|
|
306
226
|
const em = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/events$/.exec(url) : null;
|
|
307
227
|
if (em) {
|
|
308
|
-
// 二轮复审单源化:本路由**不再自读基线**——所有帧(含首帧)由 registry 按 rev 单调序 fan-out
|
|
309
|
-
// (replay-on-subscribe 给已知条目即时首帧;新条目首帧=首拍探针 ≤hotMs)。双源(自读基线 vs
|
|
310
|
-
// registry 缓冲)必然产生不可判序的回退帧,单源后该类竞序结构性不存在。getLeafId 由 registry
|
|
311
|
-
// 内部使用(main.ts 注入时已绑定宿主);路由只需 ownerOf 门。
|
|
312
228
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
313
229
|
const watch = deps.sessionWatch;
|
|
314
230
|
if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
|
|
@@ -316,17 +232,12 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
316
232
|
return;
|
|
317
233
|
}
|
|
318
234
|
const evSession = decodeURIComponent(em[1]);
|
|
319
|
-
// codex 一轮生命周期状态机:①连接坑与 close 守卫在任何 await 之前同步建立(读存储期间断连不再
|
|
320
|
-
// 漏清/漏还坑);②每个 await 之后查 closed;③所有拒绝/异常路径经同一 release。
|
|
321
235
|
if (sse.sseConnections >= (deps.sessionEventsMaxConnections ?? 256)) {
|
|
322
|
-
// 🔴 机器码而非裸 writeHead(2026-07-31 缝合审):这条 503 与 drain 的 503 走同一条流,
|
|
323
|
-
// 而 drain 的带 `errorCode:"draining"`(冻结契约)。没有码,消费端只能靠自由文本字面区分
|
|
324
|
-
// 「连接帽 ⇒ 回落 /head 轮询(自愈,保持连接策略)」与「该重连替换实例」—— 正是本仓要根除的姿势。
|
|
325
236
|
res.setHeader("retry-after", "5");
|
|
326
237
|
sendError(res, 503, "state.sse_connection_cap", "session-events connection cap reached — fall back to /head polling");
|
|
327
238
|
return;
|
|
328
239
|
}
|
|
329
|
-
sse.sseConnections++;
|
|
240
|
+
sse.sseConnections++;
|
|
330
241
|
let hb;
|
|
331
242
|
let unsubscribe;
|
|
332
243
|
let closed = false;
|
|
@@ -342,10 +253,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
342
253
|
res.on("close", cleanup);
|
|
343
254
|
req.on("close", cleanup);
|
|
344
255
|
try {
|
|
345
|
-
// 五轮复审:初始 ownerOf 也受 stall 界(存储悬死时不许无限占坑——256 坑被悬死请求耗尽=合法
|
|
346
|
-
// 客户端全 503);超时=可重试失败(503+Retry-After),坑经 cleanup 归还。六轮复审:查询本体的
|
|
347
|
-
// 并发另行有界(sseOwnerLookups,许可在底层 settle 时才还)——race 只还连接坑不还查询许可,
|
|
348
|
-
// 重连风暴不得无界堆积未决存储查询。
|
|
349
256
|
const stallBound = deps.sessionEventsStallMs ?? 60_000;
|
|
350
257
|
if (sse.sseOwnerLookups >= 64) {
|
|
351
258
|
cleanup();
|
|
@@ -355,8 +262,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
355
262
|
}
|
|
356
263
|
sse.sseOwnerLookups++;
|
|
357
264
|
const ownerP = ownerOf(evSession);
|
|
358
|
-
void ownerP.catch(() => undefined).then(() => { sse.sseOwnerLookups--; });
|
|
359
|
-
// 七轮复审:race 计时器 settle 即清——高频开合不得每次拖 60s 计时器+闭包(unref 只保进程退出面)。
|
|
265
|
+
void ownerP.catch(() => undefined).then(() => { sse.sseOwnerLookups--; });
|
|
360
266
|
let stallTimer;
|
|
361
267
|
let owner;
|
|
362
268
|
try {
|
|
@@ -370,7 +276,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
370
276
|
clearTimeout(stallTimer);
|
|
371
277
|
}
|
|
372
278
|
if (closed)
|
|
373
|
-
return;
|
|
279
|
+
return;
|
|
374
280
|
if (owner === "__stall__") {
|
|
375
281
|
cleanup();
|
|
376
282
|
res.setHeader("retry-after", "5");
|
|
@@ -387,20 +293,14 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
387
293
|
return;
|
|
388
294
|
}
|
|
389
295
|
const q = new URL(req.url ?? url, "http://x").searchParams;
|
|
390
|
-
const lastLeafId = q.get("lastLeafId");
|
|
391
|
-
// 单源发射:全部帧来自 registry(rev 单调序),连接级 lastSent 去重;首帧抑制=首值命中
|
|
392
|
-
// lastLeafId 时只记不发(后续同值仍去重,更新值照发)。
|
|
296
|
+
const lastLeafId = q.get("lastLeafId");
|
|
393
297
|
let first = true;
|
|
394
|
-
let lastAccepted;
|
|
298
|
+
let lastAccepted;
|
|
395
299
|
let lastWritten;
|
|
396
|
-
// 三轮复审背压:res.write 返回 false 即停写,只保「最新一格」待冲(latest-wins 恰是本契约),
|
|
397
|
-
// drain 冲洗;阻塞超时(缺省 60s)=消费者死/僵——断流(client 重连自愈),绝不无界积压。
|
|
398
300
|
let writeBlocked = false;
|
|
399
301
|
let blockedAt = 0;
|
|
400
302
|
let pendingHead;
|
|
401
303
|
const stallMs = deps.sessionEventsStallMs ?? 60_000;
|
|
402
|
-
// 四轮复审:**全部**写(connected/心跳注释/head 帧)走同一背压感知写手——任何一次 false 都
|
|
403
|
-
// 进 blocked 态(心跳先塞住的流同样受 stall 断流约束,不再有旁路缓冲增长)。
|
|
404
304
|
const writeRaw = (chunk) => {
|
|
405
305
|
if (closed)
|
|
406
306
|
return;
|
|
@@ -418,7 +318,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
418
318
|
});
|
|
419
319
|
}
|
|
420
320
|
}
|
|
421
|
-
catch {
|
|
321
|
+
catch { }
|
|
422
322
|
};
|
|
423
323
|
const doWrite = (leafId) => {
|
|
424
324
|
lastWritten = leafId;
|
|
@@ -433,22 +333,20 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
433
333
|
if (suppress) {
|
|
434
334
|
lastWritten = leafId;
|
|
435
335
|
return;
|
|
436
|
-
}
|
|
336
|
+
}
|
|
437
337
|
if (writeBlocked) {
|
|
438
|
-
pendingHead = { v: leafId };
|
|
338
|
+
pendingHead = { v: leafId };
|
|
439
339
|
if (Date.now() - blockedAt > stallMs) {
|
|
440
340
|
cleanup();
|
|
441
341
|
try {
|
|
442
342
|
res.end();
|
|
443
343
|
}
|
|
444
|
-
catch {
|
|
344
|
+
catch { }
|
|
445
345
|
}
|
|
446
346
|
return;
|
|
447
347
|
}
|
|
448
348
|
doWrite(leafId);
|
|
449
349
|
};
|
|
450
|
-
// 订阅先于 SSE headers(帽拒绝要保住干净 503)——headers 前到达的 replay/live 帧存一格
|
|
451
|
-
// pre 缓冲(latest-wins 覆写,单源序不破),headers 后冲洗。
|
|
452
350
|
let started = false;
|
|
453
351
|
let preVal;
|
|
454
352
|
const onHead = (leafId) => {
|
|
@@ -457,13 +355,11 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
457
355
|
else
|
|
458
356
|
emit(leafId);
|
|
459
357
|
};
|
|
460
|
-
// dropSession(session 删除,三轮复审租户轮转围栏)→ 即刻断流:旧 owner 的订阅绝不跨越
|
|
461
|
-
// delete/reclaim 边界;跨副本删除由下方心跳期 owner 复核兜底(≤心跳一拍)。
|
|
462
358
|
const onEnd = () => { cleanup(); try {
|
|
463
359
|
res.end();
|
|
464
360
|
}
|
|
465
|
-
catch {
|
|
466
|
-
unsubscribe = watch.subscribe(evSession, owner ?? null, onHead, onEnd);
|
|
361
|
+
catch { } };
|
|
362
|
+
unsubscribe = watch.subscribe(evSession, owner ?? null, onHead, onEnd);
|
|
467
363
|
if (!unsubscribe) {
|
|
468
364
|
cleanup();
|
|
469
365
|
res.setHeader("retry-after", "5");
|
|
@@ -476,8 +372,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
476
372
|
if (preVal !== undefined)
|
|
477
373
|
emit(preVal.v);
|
|
478
374
|
const hbMs = deps.sessionEventsHeartbeatMs ?? 25_000;
|
|
479
|
-
// 四轮复审:owner 复核 single-flight——上拍未归不再起新查(存储卡顿时 256 连接不得堆积
|
|
480
|
-
// 未决查询);未决超过 stallMs=复核面失效,fail-closed 断流(长活流不许在盲区里续命)。
|
|
481
375
|
let ownerCheckPending = false;
|
|
482
376
|
let ownerCheckStartedAt = 0;
|
|
483
377
|
hb = setInterval(() => {
|
|
@@ -486,22 +380,21 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
486
380
|
if (writeBlocked && Date.now() - blockedAt > stallMs) {
|
|
487
381
|
onEnd();
|
|
488
382
|
return;
|
|
489
|
-
}
|
|
383
|
+
}
|
|
490
384
|
if (!writeBlocked)
|
|
491
385
|
writeRaw(": hb\n\n");
|
|
492
386
|
if (ownerCheckPending) {
|
|
493
387
|
if (Date.now() - ownerCheckStartedAt > stallMs)
|
|
494
|
-
onEnd();
|
|
388
|
+
onEnd();
|
|
495
389
|
return;
|
|
496
390
|
}
|
|
497
|
-
// 三轮复审跨副本围栏:心跳拍复核 owner——session 被删(undefined)或换主(≠连接时 owner)即断流。
|
|
498
391
|
ownerCheckPending = true;
|
|
499
392
|
ownerCheckStartedAt = Date.now();
|
|
500
393
|
void ownerOf(evSession).then((o) => {
|
|
501
394
|
ownerCheckPending = false;
|
|
502
395
|
if (!closed && o !== owner)
|
|
503
396
|
onEnd();
|
|
504
|
-
}).catch(() => { ownerCheckPending = false;
|
|
397
|
+
}).catch(() => { ownerCheckPending = false; });
|
|
505
398
|
}, hbMs);
|
|
506
399
|
hb.unref?.();
|
|
507
400
|
}
|
|
@@ -513,7 +406,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
513
406
|
try {
|
|
514
407
|
res.end();
|
|
515
408
|
}
|
|
516
|
-
catch {
|
|
409
|
+
catch { }
|
|
517
410
|
}
|
|
518
411
|
return;
|
|
519
412
|
}
|
|
@@ -528,13 +421,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
528
421
|
}
|
|
529
422
|
if (!runOwnerOk(req, res, audit.owner))
|
|
530
423
|
return;
|
|
531
|
-
// web 会话区性能(additive — parameterless requests stay BYTE-IDENTICAL):
|
|
532
|
-
// ?tail=N → messages = last N (+ window {offset,total})
|
|
533
|
-
// ?before=<off>&limit=N → the page above a prior window (offsets stable within one floorEntryId
|
|
534
|
-
// generation — a compaction changes floorEntryId, the web refetches)
|
|
535
|
-
// ?blobCap=<bytes> → oversized string bodies truncated to a preview + message-level truncated:true
|
|
536
|
-
// (expand = refetch that ONE message via ?message=<index>, full size)
|
|
537
|
-
// ?message=<index> → SINGLE full message {message, index, floorEntryId} (the expand face)
|
|
538
424
|
const q = new URL(req.url ?? url, "http://x").searchParams;
|
|
539
425
|
const qi = (k) => {
|
|
540
426
|
const v = q.get(k);
|
|
@@ -557,7 +443,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
557
443
|
const before = qi("before");
|
|
558
444
|
const blobCap = qi("blobCap");
|
|
559
445
|
if (tail === undefined && before === undefined && blobCap === undefined) {
|
|
560
|
-
sendJson(res, 200, audit);
|
|
446
|
+
sendJson(res, 200, audit);
|
|
561
447
|
return;
|
|
562
448
|
}
|
|
563
449
|
let messages = audit.messages;
|
|
@@ -573,8 +459,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
573
459
|
return;
|
|
574
460
|
}
|
|
575
461
|
}
|
|
576
|
-
// E17 (§0.5) — POST /v1/sessions/:id/fork → {sessionId}. Branch a NEW session off the source's WHOLE history
|
|
577
|
-
// via the SessionRepo seam the service owns (sessionStorage.fork), owner-scoped exactly like GET /v1/sessions.
|
|
578
462
|
{
|
|
579
463
|
const fm = req.method === "POST" ? /^\/v1\/sessions\/([^/]+)\/fork$/.exec(url) : null;
|
|
580
464
|
if (fm) {
|
|
@@ -590,9 +474,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
590
474
|
return;
|
|
591
475
|
}
|
|
592
476
|
const sourceId = safeDecode(fm[1]);
|
|
593
|
-
// §0.5 — shape-validate the session id (uuidv7) so a crafted id can't reach the store ops as anything but
|
|
594
|
-
// a well-formed key (a second layer behind the tool-result LIKE-escape). Malformed (bad %-seq or non-uuidv7)
|
|
595
|
-
// ⇒ 400, never a store hit.
|
|
596
477
|
if (sourceId === null || !isUuidV7(sourceId)) {
|
|
597
478
|
sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
|
|
598
479
|
return;
|
|
@@ -602,38 +483,20 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
602
483
|
sendError(res, 404, "not_found.session", "session not found");
|
|
603
484
|
return;
|
|
604
485
|
}
|
|
605
|
-
// Owner gate (no existence oracle for non-owners — 404, parity with runOwnerOk): a principal caller may
|
|
606
|
-
// only fork its OWN session; a fleet-wide (system/trace) caller may fork any (ops).
|
|
607
|
-
// [4198] 窄互认第二臂:单机形下 default 主对**存量无主行**放行(三条件齐,见 legacyUnownedSessionOk)。
|
|
608
486
|
if (!scope.fleetWide && owner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, owner)) {
|
|
609
487
|
sendError(res, 404, "not_found.session", "session not found");
|
|
610
488
|
return;
|
|
611
489
|
}
|
|
612
|
-
// New-session ownership — DELIBERATE (no security issue either way; documented per adversarial-review):
|
|
613
|
-
// • principal caller → the NEW session is owned by the CALLER (scope.gateOwner): you fork your own session,
|
|
614
|
-
// you own the copy.
|
|
615
|
-
// • [4198] 窄臂放行的 default 主 → 走 principal 支 ⇒ 副本铸给 `default`(具名行)。刻意如此:副本
|
|
616
|
-
// 离开无主池,本臂**不被 fork 传播延寿**,墓碑注(存量随生命周期消亡后可整删)仍然成立。
|
|
617
|
-
// • fleet-wide (ops, fork-on-behalf) → the NEW session INHERITS the SOURCE owner (`owner ?? null`) so the
|
|
618
|
-
// fork is TENANT-owned (an ops/system fork-on-behalf hands the copy to the same tenant as the source,
|
|
619
|
-
// NOT to the ownerless/system pool) — a later attach is then governed by the normal authorizer for that
|
|
620
|
-
// tenant. (Chosen over null/system-owned: an ops fork is "give this tenant a branched copy", so tenant
|
|
621
|
-
// attribution is the intent; a system-submitted *run* forks ownerless, but a *session* fork-on-behalf
|
|
622
|
-
// is explicitly on the source tenant's behalf.)
|
|
623
490
|
const newOwner = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
|
|
624
491
|
const newId = await fork(sourceId, newOwner);
|
|
625
492
|
if (newId === null) {
|
|
626
493
|
sendError(res, 404, "not_found.session", "session not found");
|
|
627
494
|
return;
|
|
628
|
-
}
|
|
495
|
+
}
|
|
629
496
|
sendJson(res, 200, { sessionId: newId });
|
|
630
497
|
return;
|
|
631
498
|
}
|
|
632
499
|
}
|
|
633
|
-
// E21 (§0.5) — DELETE /v1/sessions/:id → {deleted}. Purge the session's conversation history (session_meta/
|
|
634
|
-
// session_event) AND the service-owned run-ledger/checkpoint/tool-result rows for it. Auth-FIRST (the scope is
|
|
635
|
-
// computed before any store touch, mirroring GET to avoid a 501-before-auth existence oracle). Owner-scoped,
|
|
636
|
-
// idempotent ({deleted:false} when nothing existed). active-run guard → 409 (don't orphan the session lock).
|
|
637
500
|
{
|
|
638
501
|
const dm = req.method === "DELETE" ? /^\/v1\/sessions\/([^/]+)$/.exec(url) : null;
|
|
639
502
|
if (dm) {
|
|
@@ -649,8 +512,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
649
512
|
return;
|
|
650
513
|
}
|
|
651
514
|
const sessionId = safeDecode(dm[1]);
|
|
652
|
-
// §0.5 — shape-validate the session id (uuidv7) so a crafted id can't reach the purge ops (a second layer
|
|
653
|
-
// behind the tool-result LIKE-escape). Malformed (bad %-seq or non-uuidv7) ⇒ 400, never a store hit.
|
|
654
515
|
if (sessionId === null || !isUuidV7(sessionId)) {
|
|
655
516
|
sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
|
|
656
517
|
return;
|
|
@@ -659,21 +520,11 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
659
520
|
if (owner === undefined) {
|
|
660
521
|
sendJson(res, 200, { deleted: false });
|
|
661
522
|
return;
|
|
662
|
-
}
|
|
663
|
-
// Foreign-owner ⇒ the SAME response as missing (a 404 here vs 200 above was an existence
|
|
664
|
-
// oracle — a prober could distinguish "absent" from "another tenant's"). deleted:false is also honest
|
|
665
|
-
// (nothing was deleted) and keeps the route idempotent-shaped for both cases.
|
|
666
|
-
// [4198] 窄互认第二臂:不放行时,存量无主会话对 default 主是**静默假成功**(用户永远删不掉自己的历史)。
|
|
667
|
-
// 放行后 purge 的 owner 守卫仍传**行的真 owner**(下面的 `owner`,即 null)——SQL 侧 null-safe 属主门不被放宽。
|
|
523
|
+
}
|
|
668
524
|
if (!scope.fleetWide && owner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, owner)) {
|
|
669
525
|
sendJson(res, 200, { deleted: false });
|
|
670
526
|
return;
|
|
671
527
|
}
|
|
672
|
-
// Don't orphan the single-active-run lock: refuse while a run is live on this session (409 over a
|
|
673
|
-
// half-purge that strands a task_active claim). A terminal/suspended/parked session deletes cleanly. This
|
|
674
|
-
// is a cheap PRE-check; the AUTHORITATIVE no-active-run gate is re-asserted transactionally inside the
|
|
675
|
-
// runs-ledger purge (FOR UPDATE on task_active), which closes the check→purge TOCTOU and also returns
|
|
676
|
-
// `{ active }` to 409 if a createRun raced in between.
|
|
677
528
|
const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
|
|
678
529
|
if (activeTaskId) {
|
|
679
530
|
sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId });
|
|
@@ -688,12 +539,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
688
539
|
return;
|
|
689
540
|
}
|
|
690
541
|
}
|
|
691
|
-
// E6 — GET/PUT /v1/sessions/:id/policy: read / write operator-tightened per-session tool-permission rules (core
|
|
692
|
-
// reads them at prepare-time, SUBTRACT-only). Owner-gated + the operator flag FROZEN from the verified principal
|
|
693
|
-
// (explicitOperatorOk(gatedPrincipal), NEVER the spoofable header — the steer.trusted privilege-escalation rule):
|
|
694
|
-
// a non-operator may only TIGHTEN its OWN session (a loosen → loosen_forbidden); an operator may loosen + act
|
|
695
|
-
// cross-tenant. The target key = the session OWNER (the principal core reads rules under). SessionPolicyError →
|
|
696
|
-
// conflict:409 (stale expectedRev) / loosen_forbidden:403. 404 (not 403) on a non-owner — no existence oracle.
|
|
697
542
|
{
|
|
698
543
|
const pm = req.method === "GET" || req.method === "PUT" ? /^\/v1\/sessions\/([^/]+)\/policy$/.exec(url) : null;
|
|
699
544
|
if (pm) {
|
|
@@ -717,11 +562,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
717
562
|
if (owner === undefined) {
|
|
718
563
|
sendError(res, 404, "not_found.session", "session not found");
|
|
719
564
|
return;
|
|
720
|
-
}
|
|
721
|
-
// E6 (review): rules MUST be keyed by the principal core reads at run time (getRules(sessionId, spec.principal)).
|
|
722
|
-
// For an OWNED session that is the owner (a task on it runs as the owner). An OWNERLESS session (owner=null,
|
|
723
|
-
// REQUIRE_PRINCIPAL=false) has no stable run-principal — a task attaching any principal would read a DIFFERENT
|
|
724
|
-
// key, silently bypassing the rule (fail-OPEN). Refuse rather than store a rule that won't apply.
|
|
565
|
+
}
|
|
725
566
|
if (owner === null) {
|
|
726
567
|
sendError(res, 409, "conflict.session_ownerless", "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless");
|
|
727
568
|
return;
|
|
@@ -730,16 +571,12 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
730
571
|
if (!operator && owner !== (principal ?? null)) {
|
|
731
572
|
sendError(res, 404, "not_found.session", "session not found");
|
|
732
573
|
return;
|
|
733
|
-
}
|
|
734
|
-
const target = owner;
|
|
574
|
+
}
|
|
575
|
+
const target = owner;
|
|
735
576
|
if (req.method === "GET") {
|
|
736
|
-
// BC-3 (shell §I): the 200 is ALWAYS a non-null StoredSessionRules with a load-bearing `rev` (the PUT CAS
|
|
737
|
-
// baseline). An owned session with NO policy yet → getRules null → return the empty-at-rev-0 record (the
|
|
738
|
-
// store reads a missing row as rev 0 = expectedRev:0 create-once), never `{rules:null}`.
|
|
739
577
|
sendJson(res, 200, { rules: (await store.getRules(sessionId, target)) ?? { rev: 0 } });
|
|
740
578
|
return;
|
|
741
579
|
}
|
|
742
|
-
// PUT — validate + pick the 5 known string[] rule fields (reject malformed fail-loud); CAS + tighten gate in the store.
|
|
743
580
|
let body;
|
|
744
581
|
try {
|
|
745
582
|
body = (await readJson(req));
|
|
@@ -781,11 +618,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
781
618
|
return;
|
|
782
619
|
}
|
|
783
620
|
}
|
|
784
|
-
// ── workspace 浏览面(#3,docs/DESIGN-workspace-browse.md;[1882] 征询 → [1894] cli 表态四点细化)────────
|
|
785
|
-
// E19 快照店的只读投影:list/tree/file/archive 四端点,零新 store。owner 门=sync 面 PULL 同款
|
|
786
|
-
// (session 必须存在且属调用方,404 无 oracle);501 谓词=快照店三读面(listKeys/exportManifest/
|
|
787
|
-
// getBlob)+ ownerOf 齐(capabilities.workspace 同源)。`:key=latest` 别名=最新快照(键=entryId,
|
|
788
|
-
// uuidv7 时间序 ⇒ 字典序最大即最新)。
|
|
789
621
|
{
|
|
790
622
|
const wm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/workspace(?:\/([^/]+)(?:\/(tree|file|archive))?)?$/.exec(url) : null;
|
|
791
623
|
if (wm) {
|
|
@@ -806,16 +638,13 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
806
638
|
return;
|
|
807
639
|
}
|
|
808
640
|
const wsOwner = await ownerOf(wsSession);
|
|
809
|
-
// PULL 门:必须存在且属己,无 oracle。[4198] 窄互认第二臂同样覆盖本读面(存量会话的工作区浏览)。
|
|
810
641
|
if (wsOwner === undefined || (!scope.fleetWide && wsOwner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, wsOwner))) {
|
|
811
642
|
sendError(res, 404, "not_found.session", "session not found");
|
|
812
643
|
return;
|
|
813
644
|
}
|
|
814
645
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
815
|
-
const keys = (await fs.listKeys(wsSession)).sort().reverse();
|
|
646
|
+
const keys = (await fs.listKeys(wsSession)).sort().reverse();
|
|
816
647
|
const sub = wm[3];
|
|
817
|
-
// GET …/workspace — 列快照(新→旧;?limit 1..100 默认 20)。files=manifest 行数;bytes 仅在
|
|
818
|
-
// blobSizes 面在场时给(local 店缺席=诚实省略,[1894]② additive 口径)。
|
|
819
648
|
if (wm[2] === undefined) {
|
|
820
649
|
const limRaw = q.get("limit");
|
|
821
650
|
if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 100)) {
|
|
@@ -823,12 +652,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
823
652
|
return;
|
|
824
653
|
}
|
|
825
654
|
const lim = limRaw === null ? 20 : Number(limRaw);
|
|
826
|
-
// [2400] CAPS-OPS-6(clay 裁 2026-08-03,最优设计不留旧语义):
|
|
827
|
-
// · `?before=<key>`(exclusive)游标——keys 是 uuidv7 字典序 desc(新→旧),before 取更旧段;
|
|
828
|
-
// · `total` = listKeys 口径的**快照总数**(与 tree 腿的 total 同名同义;此前=页内行数,同名反义
|
|
829
|
-
// 且超过 limit 的快照永远取不到)。竞态窗口内正被 reap 的 ghost 键会短暂多计 1,下一拍自愈;
|
|
830
|
-
// · `nextBefore` 仅在还有更旧的 key 时给(=本页最后一个 key);
|
|
831
|
-
// · 复审 A-MED 的保护不变:latest 与**展示行**同源(ghost 不当 latest)、页行掉 ghost 不空报。
|
|
832
655
|
const beforeRaw = q.get("before");
|
|
833
656
|
const scoped = beforeRaw !== null ? keys.filter((k) => k < beforeRaw) : keys;
|
|
834
657
|
const page = scoped.slice(0, lim);
|
|
@@ -836,7 +659,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
836
659
|
for (const k of page) {
|
|
837
660
|
const m = await fs.exportManifest(wsSession, k);
|
|
838
661
|
if (m === null)
|
|
839
|
-
continue;
|
|
662
|
+
continue;
|
|
840
663
|
let bytes;
|
|
841
664
|
if (typeof fs.blobSizes === "function") {
|
|
842
665
|
const sizes = await fs.blobSizes([...new Set(m.values())]);
|
|
@@ -857,7 +680,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
857
680
|
});
|
|
858
681
|
return;
|
|
859
682
|
}
|
|
860
|
-
// :key 解析(latest 别名);快照必须真存在(manifest 非 null)。
|
|
861
683
|
const keyRaw = safeDecode(wm[2]);
|
|
862
684
|
if (keyRaw === null) {
|
|
863
685
|
sendError(res, 400, "request.path_malformed", "malformed snapshot key");
|
|
@@ -870,7 +692,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
870
692
|
return;
|
|
871
693
|
}
|
|
872
694
|
if (sub === "tree") {
|
|
873
|
-
// [1894]②:entries 带 {path, size?, hash};?limit 默认 500 cap 2000、?offset 分页;path 序稳定。
|
|
874
695
|
const limRaw = q.get("limit");
|
|
875
696
|
if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 2000)) {
|
|
876
697
|
sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..2000");
|
|
@@ -907,8 +728,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
907
728
|
return;
|
|
908
729
|
}
|
|
909
730
|
const cap = deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024;
|
|
910
|
-
// [1894]①:超限=413 结构化信封(web 渲「文件过大,下载查看」卡)。size 可先知(SQL 索引)时
|
|
911
|
-
// 先拒后拉——绝不为判超限吸整块字节。
|
|
912
731
|
if (typeof fs.blobSizes === "function") {
|
|
913
732
|
const known = (await fs.blobSizes([hash])).get(hash);
|
|
914
733
|
if (known !== undefined && known > cap) {
|
|
@@ -925,8 +744,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
925
744
|
sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { sizeBytes: bytes.byteLength, limit: cap });
|
|
926
745
|
return;
|
|
927
746
|
}
|
|
928
|
-
// [1894]③:content-type 按扩展名(保守表,未知=octet-stream)+ 二进制判别头(首 8KiB NUL/控制
|
|
929
|
-
// 字节嗅探)。nosniff 恒带(内容是 agent 产物=不可信,绝不让浏览器自嗅成可执行类型)。
|
|
930
747
|
const ct = workspaceContentType(pathParam);
|
|
931
748
|
const bin = looksBinary(bytes);
|
|
932
749
|
res.writeHead(200, {
|
|
@@ -939,10 +756,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
939
756
|
return;
|
|
940
757
|
}
|
|
941
758
|
if (sub === "archive") {
|
|
942
|
-
// 流式 tar(ustar,src/http/tar.ts 零依赖)。开流前三道预检(头没发,还能诚实报错):
|
|
943
|
-
// ① 路径全部可表示(>255B/不可拆 ⇒ 422 点名);② blob 全在(hasBlobs 单查;缺 ⇒ 502 可重试);
|
|
944
|
-
// ③ 之后逐文件 getBlob 流式写+背压。中途 blob 消失(reap 竞态)⇒ 已在流中,destroy 连接
|
|
945
|
-
// (诚实截断——tar 尾块缺失,消费端可判损)。
|
|
946
759
|
const entries = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
947
760
|
const bad = entries.filter(([p]) => splitTarPath(p) === null).map(([p]) => p);
|
|
948
761
|
if (bad.length > 0) {
|
|
@@ -965,10 +778,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
965
778
|
const write = async (buf) => {
|
|
966
779
|
if (res.writableEnded || res.destroyed)
|
|
967
780
|
throw new Error("client gone");
|
|
968
|
-
// 🔴 复审 F5:drain 必须与 close/error **竞速**——socket 被销毁后 `drain` 永不再来,只等
|
|
969
|
-
// 它会让本协程永久 pending(入口的 destroyed 守卫挡不住「已经卡在 await 里」这一态)。
|
|
970
|
-
// 今天的缓和事实是引用图随连接一起变垃圾、admittedInflight 由 res.on("close") 独立释放;
|
|
971
|
-
// 但任何将来加在这条腿上的 finally 清理都会静默不执行 —— 一行竞速根除这个类别。
|
|
972
781
|
if (!res.write(buf)) {
|
|
973
782
|
await new Promise((resolve, reject) => {
|
|
974
783
|
const onDrain = () => { cleanup(); resolve(); };
|
|
@@ -993,11 +802,10 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
993
802
|
res.end();
|
|
994
803
|
}
|
|
995
804
|
catch {
|
|
996
|
-
res.destroy();
|
|
805
|
+
res.destroy();
|
|
997
806
|
}
|
|
998
807
|
return;
|
|
999
808
|
}
|
|
1000
|
-
// 裸 :key 无动词 ⇒ 404 指路(不猜语义)。
|
|
1001
809
|
sendError(res, 404, "request.route_unsupported", "unknown workspace verb — use /tree, /file?path=, or /archive");
|
|
1002
810
|
return;
|
|
1003
811
|
}
|