@sema-agent/server 7.151.0 → 7.152.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 +15 -3
- package/README.zh-CN.md +10 -2
- package/USAGE.md +93 -64
- package/deploy/sema-up/README.md +5 -4
- package/deploy/sema-up/cluster-up.sh +6 -6
- package/deploy/sema-up/kube-up.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +36 -8
- package/deploy/sema-up/machines.example.yaml +3 -1
- package/deploy/sema-up/sema-up.sh +22 -16
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-ask-machine.d.ts +4 -10
- package/dist/approval-ask-retention.d.ts +1 -1
- package/dist/approval-card.d.ts +10 -12
- package/dist/approval-gate-currency.d.ts +8 -8
- package/dist/approval-gate-currency.js +3 -4
- package/dist/approval-hmac.d.ts +17 -1
- package/dist/approval-hmac.js +7 -1
- package/dist/approval-queue-kind.d.ts +1 -1
- package/dist/approval.d.ts +2 -2
- package/dist/approval.js +2 -2
- package/dist/bind-host.d.ts +1 -3
- package/dist/bind-host.js +2 -3
- package/dist/boot/coordinators.d.ts +1 -1
- package/dist/boot/coordinators.js +9 -10
- package/dist/boot/device-lane.js +2 -0
- package/dist/boot/execution-env.d.ts +1 -0
- package/dist/boot/execution-env.js +31 -5
- package/dist/boot/leader.d.ts +31 -30
- package/dist/boot/leader.js +38 -28
- package/dist/boot/leg-path-adjudication.d.ts +40 -0
- package/dist/boot/leg-path-adjudication.js +18 -0
- package/dist/boot/parked-revive-gate.js +3 -15
- package/dist/boot/reapers.d.ts +1 -1
- package/dist/boot/reapers.js +1 -1
- package/dist/boot/resolve-spec.js +12 -26
- package/dist/boot/runner-deps.d.ts +39 -35
- package/dist/boot/runner-deps.js +92 -89
- package/dist/boot/shutdown.d.ts +6 -0
- package/dist/boot/shutdown.js +10 -4
- package/dist/boot/stage-02-web-admin-pull.js +4 -5
- package/dist/boot/stage-05-execution-env.js +2 -1
- package/dist/boot/stage-06-runners.d.ts +2 -0
- package/dist/boot/stage-06-runners.js +10 -4
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +8 -0
- package/dist/boot/stage-10-http-server.js +11 -15
- package/dist/boot/stores.js +7 -4
- package/dist/boot/web-admin.js +2 -7
- package/dist/capabilities/approvals-push.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +28 -13
- package/dist/capabilities/scenarios.js +2 -2
- package/dist/capability-facts.d.ts +22 -20
- package/dist/capability-facts.js +18 -34
- package/dist/card-rule-offers.d.ts +32 -0
- package/dist/card-rule-offers.js +130 -0
- package/dist/config-catalog.js +12 -11
- package/dist/config-dir-trust.js +4 -2
- package/dist/config-invariants.d.ts +18 -7
- package/dist/config-invariants.js +51 -8
- package/dist/config-lkg.d.ts +5 -19
- package/dist/config-lkg.js +8 -62
- package/dist/config-provider.d.ts +1 -1
- package/dist/config-root-migration.d.ts +2 -2
- package/dist/config-types.d.ts +28 -38
- package/dist/config.d.ts +7 -5
- package/dist/config.js +55 -16
- package/dist/control-socket.d.ts +51 -0
- package/dist/control-socket.js +115 -0
- package/dist/deployment-governance.d.ts +2 -1
- package/dist/device-ws-hub.d.ts +7 -0
- package/dist/device-ws-hub.js +7 -1
- package/dist/elicitation.js +2 -2
- package/dist/entry-problems.d.ts +40 -3
- package/dist/entry-problems.js +3 -1
- package/dist/execution-env-refusal.d.ts +8 -2
- package/dist/execution-env-refusal.js +3 -0
- package/dist/fleet/engine-signal.d.ts +12 -29
- package/dist/fleet/engine-signal.js +1 -7
- package/dist/fleet-client.js +2 -5
- package/dist/gate-settlement.d.ts +1 -8
- package/dist/gate-settlement.js +0 -4
- package/dist/history-anchor.js +1 -1
- package/dist/hitl-delivery.d.ts +2 -3
- package/dist/hitl-delivery.js +0 -2
- package/dist/hooks/hook-llm.js +4 -2
- package/dist/hooks/hook-runner.d.ts +3 -2
- package/dist/hooks/hook-runner.js +16 -1
- package/dist/host-decision.d.ts +11 -11
- package/dist/http/active-run-conflict.d.ts +35 -2
- package/dist/http/active-run-conflict.js +48 -19
- package/dist/http/approval-sweeps.d.ts +1 -1
- package/dist/http/approval-sweeps.js +5 -2
- package/dist/http/checkpoint-ctx.d.ts +2 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/dispatch.d.ts +6 -2
- package/dist/http/dispatch.js +25 -5
- package/dist/http/durable-run-leg.js +0 -1
- package/dist/http/health-reader.d.ts +16 -9
- package/dist/http/health-reader.js +2 -1
- package/dist/http/health-self-report.js +1 -1
- package/dist/http/owner-access.d.ts +2 -3
- package/dist/http/owner-access.js +3 -4
- package/dist/http/principal-gate.d.ts +49 -21
- package/dist/http/principal-gate.js +61 -11
- package/dist/http/request-channel.d.ts +23 -0
- package/dist/http/request-channel.js +9 -0
- package/dist/http/request-guards.js +17 -2
- package/dist/http/resume-ctx-read.js +20 -3
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +111 -41
- package/dist/http/route-ctx.d.ts +73 -7
- package/dist/http/route-ctx.js +17 -0
- package/dist/http/route-table.d.ts +4 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-config-refresh.js +3 -3
- package/dist/http/routes/admin-drain.d.ts +3 -0
- package/dist/http/routes/admin-drain.js +7 -7
- package/dist/http/routes/admin-memory-optout.js +3 -3
- package/dist/http/routes/adoption.js +3 -5
- package/dist/http/routes/approvals-assistant.d.ts +2 -1
- package/dist/http/routes/approvals-assistant.js +95 -48
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +30 -5
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +19 -7
- package/dist/http/routes/images.js +10 -10
- package/dist/http/routes/memory-bundle.js +3 -3
- package/dist/http/routes/memory-compliance.js +3 -3
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +3 -3
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/observability.js +3 -3
- package/dist/http/routes/retention-ops.js +3 -3
- package/dist/http/routes/rules.js +5 -5
- package/dist/http/routes/run-permission-mode.d.ts +2 -1
- package/dist/http/routes/run-permission-mode.js +2 -2
- package/dist/http/routes/runs.d.ts +4 -9
- package/dist/http/routes/runs.js +73 -33
- package/dist/http/routes/session-sync.d.ts +18 -6
- package/dist/http/routes/session-sync.js +38 -36
- package/dist/http/routes/sessions.js +20 -9
- package/dist/http/routes/tasks.js +28 -37
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +9 -3
- package/dist/http/server.js +15 -3
- package/dist/http/submit-ctx.d.ts +1 -1
- package/dist/http/submit-ctx.js +2 -0
- package/dist/human-facing-args.d.ts +33 -0
- package/dist/human-facing-args.js +33 -0
- package/dist/index.d.ts +1 -1
- package/dist/k8s-api-credentials.d.ts +29 -0
- package/dist/k8s-api-credentials.js +25 -0
- package/dist/lane-home-probe.d.ts +1 -1
- package/dist/lane-home-probe.js +2 -1
- package/dist/leader/diffup.d.ts +19 -0
- package/dist/leader/diffup.js +7 -0
- package/dist/leader/leader.d.ts +114 -10
- package/dist/leader/leader.js +69 -5
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +2 -1
- package/dist/leader/run-entry.js +1 -1
- package/dist/leader/wire.d.ts +42 -132
- package/dist/leader/wire.js +81 -67
- package/dist/memory-scope.d.ts +20 -9
- package/dist/memory-scope.js +15 -3
- package/dist/model-entry-headers.d.ts +9 -10
- package/dist/model-entry-headers.js +0 -4
- package/dist/model-entry-refusal.d.ts +7 -4
- package/dist/model-entry-refusal.js +11 -5
- package/dist/model-plane-face.d.ts +1 -1
- package/dist/model-plane-face.js +3 -1
- package/dist/model-route-endpoint.d.ts +13 -7
- package/dist/model-route-endpoint.js +14 -11
- package/dist/model-select.d.ts +22 -0
- package/dist/model-select.js +9 -0
- package/dist/model-unreachable-notice.d.ts +11 -0
- package/dist/model-unreachable-notice.js +4 -4
- package/dist/object-store-buckets.d.ts +640 -6
- package/dist/object-store-buckets.js +166 -0
- package/dist/observability/fail-open.d.ts +20 -16
- package/dist/observability/fail-open.js +20 -16
- package/dist/observability/logger.js +3 -1
- package/dist/operator-roster.d.ts +68 -0
- package/dist/operator-roster.js +32 -0
- package/dist/orchestration/workflow-park-decision.d.ts +1 -1
- package/dist/outbound-dispatcher.d.ts +71 -8
- package/dist/outbound-dispatcher.js +48 -7
- package/dist/owner-faces.d.ts +1 -1
- package/dist/parked-decide.d.ts +2 -2
- package/dist/permission-mode-admission.d.ts +59 -26
- package/dist/permission-mode-admission.js +37 -10
- package/dist/plugins/approval-ask-store-file.d.ts +4 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
- package/dist/plugins/approval-ask-store-sql.js +12 -1
- package/dist/plugins/blob-backend.d.ts +83 -7
- package/dist/plugins/blob-backend.js +57 -6
- package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
- package/dist/plugins/checkpoint-index-columns.js +3 -6
- package/dist/plugins/checkpoint-store-face.d.ts +119 -61
- package/dist/plugins/checkpoint-store-face.js +134 -55
- package/dist/plugins/landed-attendants.d.ts +2 -2
- package/dist/plugins/local-checkpoint-store.d.ts +13 -3
- package/dist/plugins/local-checkpoint-store.js +38 -49
- package/dist/plugins/one-time-migrations.d.ts +51 -21
- package/dist/plugins/one-time-migrations.js +67 -45
- package/dist/plugins/pg-pool.js +11 -9
- package/dist/plugins/remote-env-host.d.ts +29 -10
- package/dist/plugins/remote-env-host.js +99 -19
- package/dist/plugins/remote-env-k8s.d.ts +68 -2
- package/dist/plugins/remote-env-k8s.js +107 -24
- package/dist/plugins/replica-registration.d.ts +1 -1
- package/dist/plugins/replica-registration.js +1 -1
- package/dist/plugins/s3-presign.d.ts +23 -0
- package/dist/plugins/s3-presign.js +37 -3
- package/dist/plugins/scheduler-support.d.ts +2 -2
- package/dist/plugins/scheduler-support.js +0 -3
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-file-ledger.d.ts +2 -2
- package/dist/plugins/send-file-ledger.js +2 -2
- package/dist/plugins/send-user-file.d.ts +3 -4
- package/dist/plugins/send-user-file.js +56 -52
- package/dist/plugins/session-blob-claim-token.d.ts +15 -0
- package/dist/plugins/session-blob-claim-token.js +20 -0
- package/dist/plugins/session-blob-namespace.d.ts +46 -0
- package/dist/plugins/session-blob-namespace.js +116 -0
- package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
- package/dist/plugins/snapshot-bucket-presence.js +8 -2
- package/dist/plugins/sql-json-column.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +44 -2
- package/dist/plugins/store-backend.js +75 -53
- package/dist/plugins/task-attachment-store.d.ts +2 -1
- package/dist/plugins/task-attachment-store.js +5 -1
- package/dist/plugins/tidb-pool.js +19 -24
- package/dist/plugins/workspace-custody.d.ts +4 -3
- package/dist/question.d.ts +2 -1
- package/dist/question.js +5 -13
- package/dist/request-form.d.ts +6 -1
- package/dist/request-form.js +4 -1
- package/dist/rule-issue-reading.d.ts +1 -1
- package/dist/rules-consent.d.ts +1 -1
- package/dist/run-local.js +8 -9
- package/dist/runs.d.ts +17 -8
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +2 -2
- package/dist/sema-config-dir.d.ts +2 -1
- package/dist/session-sync.d.ts +27 -11
- package/dist/session-sync.js +15 -7
- package/dist/stores-sql/blob-index-sql.d.ts +30 -9
- package/dist/stores-sql/blob-index-sql.js +40 -21
- package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
- package/dist/stores-sql/checkpoint-store-sql.js +70 -146
- package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
- package/dist/stores-sql/file-history-store-sql.js +100 -28
- package/dist/stores-sql/retention-store-sql.js +3 -9
- package/dist/task-settings.d.ts +6 -5
- package/dist/task-settings.js +3 -3
- package/dist/tool-approval.d.ts +342 -518
- package/dist/tool-approval.js +498 -1207
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/ledger-events.d.ts +3 -3
- package/dist/trace/project.d.ts +2 -3
- package/dist/trace/redact.js +3 -2
- package/dist/trace/self-minted-wire.d.ts +287 -157
- package/dist/trace/self-minted-wire.js +93 -48
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/read-warnings.js +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/dist/workspace-space.d.ts +10 -4
- package/dist/workspace-space.js +4 -1
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
- package/package.json +6 -2
- package/scripts/embed-minimal.mjs +3 -3
- package/wire-changes/server@7.152.0.json +900 -0
- package/dist/config-d-migration.d.ts +0 -37
- package/dist/config-d-migration.js +0 -60
|
@@ -6,6 +6,28 @@ import { recordFailOpen } from "../observability/fail-open.js";
|
|
|
6
6
|
import { refusalBody } from "./refusal-body.js";
|
|
7
7
|
import { trustedCheckpointId } from "../checkpoint-identity.js";
|
|
8
8
|
export const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
|
|
9
|
+
function holderOf(holder) {
|
|
10
|
+
return holder ? holder : null;
|
|
11
|
+
}
|
|
12
|
+
export async function occupantOf(runStore, holder) {
|
|
13
|
+
const activeTaskId = holderOf(holder);
|
|
14
|
+
if (activeTaskId === null || runStore === undefined)
|
|
15
|
+
return { activeTaskId };
|
|
16
|
+
const status = (await runFactsOf(runStore, activeTaskId))?.status;
|
|
17
|
+
return status !== undefined && (status === "running" || isParkedRunStatus(status)) ? { activeTaskId, activeTaskStatus: status } : { activeTaskId };
|
|
18
|
+
}
|
|
19
|
+
export async function refusalOccupantOf(runStore, holder) {
|
|
20
|
+
try {
|
|
21
|
+
return await occupantOf(runStore, holder);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
recordFailOpen("server.approvals.active-run-conflict-material-unavailable");
|
|
25
|
+
return { activeTaskId: holderOf(holder) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function readSessionOccupant(runStore, sessionId) {
|
|
29
|
+
return occupantOf(runStore, await runStore.getActiveTaskId(sessionId));
|
|
30
|
+
}
|
|
9
31
|
export function resumeEntryForGate(kind, ids) {
|
|
10
32
|
switch (kind) {
|
|
11
33
|
case "human":
|
|
@@ -42,24 +64,31 @@ function activeRunConflictBody(message, facts) {
|
|
|
42
64
|
return body;
|
|
43
65
|
}
|
|
44
66
|
export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
45
|
-
const baseFacts = { activeTaskId: activeTaskId
|
|
67
|
+
const baseFacts = { activeTaskId: holderOf(activeTaskId) };
|
|
46
68
|
const base = activeRunConflictBody(ACTIVE_RUN_CONFLICT_BASE_TEXT, baseFacts);
|
|
47
|
-
if (
|
|
69
|
+
if (baseFacts.activeTaskId === null)
|
|
48
70
|
return base;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
const holder = baseFacts.activeTaskId;
|
|
72
|
+
const occupant = await refusalOccupantOf(deps.runStore, holder);
|
|
73
|
+
const status = occupant.activeTaskStatus;
|
|
74
|
+
if (status === undefined)
|
|
75
|
+
return activeRunConflictBody(ACTIVE_RUN_CONFLICT_BASE_TEXT, occupant);
|
|
76
|
+
if (status === "running") {
|
|
77
|
+
let lastAt;
|
|
78
|
+
try {
|
|
79
|
+
lastAt = deps.turnActivity?.(holder);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
recordFailOpen("server.approvals.active-run-conflict-material-unavailable");
|
|
60
83
|
}
|
|
84
|
+
return activeRunConflictBody("session already has an active run — POST /v1/runs/{activeTaskId}/steer injects a message into the running turn (queued, applied at the next turn boundary), or POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", {
|
|
85
|
+
...occupant,
|
|
86
|
+
...(lastAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastAt) } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
let pendingGate;
|
|
90
|
+
try {
|
|
61
91
|
const cs = deps.checkpointStore;
|
|
62
|
-
let pendingGate;
|
|
63
92
|
if (cs?.findPendingTokenBySession) {
|
|
64
93
|
const runBinding = { gateKinds: LIVE_RUN_GATE_KINDS, axis: "run" };
|
|
65
94
|
const scope = (await cs.peekPendingScope?.(sessionId, runBinding)) ?? undefined;
|
|
@@ -68,7 +97,7 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
|
68
97
|
const gate = row?.gate;
|
|
69
98
|
const kind = gate?.kind;
|
|
70
99
|
const cardSessionId = row?.sessionId;
|
|
71
|
-
const decidePath = kind && cardSessionId ? resumeEntryForGate(kind, { sessionId: cardSessionId, taskId:
|
|
100
|
+
const decidePath = kind && cardSessionId ? resumeEntryForGate(kind, { sessionId: cardSessionId, taskId: holder }) : null;
|
|
72
101
|
const governanceForced = governanceOriginOf(gate, deps.governance);
|
|
73
102
|
const checkpointId = trustedCheckpointId(row?.checkpointId, token);
|
|
74
103
|
if (kind && decidePath) {
|
|
@@ -80,13 +109,13 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
|
80
109
|
};
|
|
81
110
|
}
|
|
82
111
|
}
|
|
83
|
-
return activeRunConflictBody(pendingGate
|
|
84
|
-
? `session already has an active run — it is parked waiting for a decision (${status}); resolve it via POST ${pendingGate.decidePath}, or POST /v1/runs/{activeTaskId}/cancel abandons it`
|
|
85
|
-
: `session already has an active run — it is parked (${status}); POST /v1/runs/{activeTaskId}/cancel abandons it (its pending decision could not be located on this instance)`, { ...baseFacts, activeTaskStatus: status, ...(pendingGate ? { pendingGate } : {}) });
|
|
86
112
|
}
|
|
87
113
|
catch {
|
|
88
114
|
recordFailOpen("server.approvals.active-run-conflict-material-unavailable");
|
|
89
|
-
|
|
115
|
+
pendingGate = undefined;
|
|
90
116
|
}
|
|
117
|
+
return activeRunConflictBody(pendingGate
|
|
118
|
+
? `session already has an active run — it is parked waiting for a decision (${status}); resolve it via POST ${pendingGate.decidePath}, or POST /v1/runs/{activeTaskId}/cancel abandons it`
|
|
119
|
+
: `session already has an active run — it is parked (${status}); POST /v1/runs/{activeTaskId}/cancel abandons it (its pending decision could not be located on this instance)`, { ...occupant, ...(pendingGate ? { pendingGate } : {}) });
|
|
91
120
|
}
|
|
92
121
|
//# sourceMappingURL=active-run-conflict.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 审批扫腿([ref] 切片:`http/server.ts` 5196–5488 逐字搬家,正文一字未改)——两条不经 HTTP 路由的赎回口:
|
|
3
3
|
* ①`denyExpiredApprovals`([ref] D-D,SLA 到期的 deny-sweep,main.ts 的 reaper 按拍调);
|
|
4
|
-
* ②`redeemParkedAsk`([ref]
|
|
4
|
+
* ②`redeemParkedAsk`([ref],决断口停驻臂的 PARKED 赎回席,装配层接进协调器的晚绑座)。
|
|
5
5
|
* 两者都走 resume 家族的 `resumeCheckpoint`,所以它们的属主是「谁在无 HTTP 请求的情况下替人做决定」这件事,
|
|
6
6
|
* 不是请求流水线。`createWallClockJumpGuard` 建的 `denySweepClockGuard` 是 ① 的私有件,随它同居。
|
|
7
7
|
* ctx 字段 3 只(`deps` / `resumeCheckpoint` / `decisionRowAdmits`),机器算(closure-free-vars)与手抄一致。
|
|
@@ -19,6 +19,7 @@ import { storedBodyReplayRefusal } from "./stored-body-replay.js";
|
|
|
19
19
|
import { refusalBody } from "./refusal-body.js";
|
|
20
20
|
import { CARD_EXPIRE_OUTCOME, expireCardUnderStoredScope } from "../card-settle-stored-scope.js";
|
|
21
21
|
import { DENY_SWEEP_PAGE_ROWS } from "../plugins/checkpoint-store-face.js";
|
|
22
|
+
import { ruleReceiptOf } from "./route-ctx.js";
|
|
22
23
|
const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
|
|
23
24
|
function blockedScalars(body) {
|
|
24
25
|
const out = {};
|
|
@@ -258,8 +259,10 @@ export function createApprovalSweeps(ctx) {
|
|
|
258
259
|
...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
|
|
259
260
|
};
|
|
260
261
|
const remember = req.remember === "session" ? { grantedBy: req.principal ?? null, laneCannotRecord: "decide_once" } : undefined;
|
|
261
|
-
const
|
|
262
|
-
|
|
262
|
+
const persistRule = req.persistRule !== undefined ? { slot: req.persistRule, inputWasEdited: req.updatedInput !== undefined } : undefined;
|
|
263
|
+
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.decider, req.principal), req.httpReq, undefined, binding, remember, true, answerer, undefined, persistRule);
|
|
264
|
+
const rule = out.status >= 200 && out.status < 300 ? ruleReceiptOf(out, persistRule) : undefined;
|
|
265
|
+
return { status: out.status, body: out.body, ...(remember !== undefined ? { rememberApplied: out.rememberApplied === true } : {}), ...(rule !== undefined ? { rule } : {}) };
|
|
263
266
|
}
|
|
264
267
|
return { denyExpiredApprovals, redeemParkedAsk };
|
|
265
268
|
}
|
|
@@ -44,6 +44,7 @@ import type { CheckpointCtx } from "./route-ctx.js";
|
|
|
44
44
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
45
45
|
import type { ProjectContext } from "../project-context.js";
|
|
46
46
|
import type { MemorySeed } from "../project-registry.js";
|
|
47
|
+
import type { PermissionModeWrite } from "../permission-mode-admission.js";
|
|
47
48
|
import type { InertToolListEntry } from "../rule-entry-list.js";
|
|
48
49
|
import { type Refusal } from "./refusal-body.js";
|
|
49
50
|
/** allow 层读法从这一版起(7.117.0):此前的写者对 `settings.permissions.allow` 是另一种读法(≤7.114.x 名单 / 7.115.0–7.116.x 表态)。规则见顶注「allow 读法问」。 */
|
|
@@ -82,6 +83,7 @@ export declare function buildCheckpointCtx(fields: {
|
|
|
82
83
|
projectContext?: ProjectContext | undefined;
|
|
83
84
|
memorySeed?: MemorySeed | null | undefined;
|
|
84
85
|
writerVersion: string;
|
|
86
|
+
permissionModeWrite: PermissionModeWrite | undefined;
|
|
85
87
|
}, sealer: CheckpointCtxSealer | undefined, sessionId: string): SealedCheckpointCtx;
|
|
86
88
|
/**
|
|
87
89
|
* 退役请求键问:**退役的请求键**闭集 = 两张处置表之并(新提交在键闭集门 400;存体里还带着它 = 这份体是更早的写者写下的)。键住在哪张表就是它的处置格,规则全文见头注「退役请求键问」。
|
|
@@ -45,6 +45,7 @@ export function buildCheckpointCtx(fields, sealer, sessionId) {
|
|
|
45
45
|
...(fields.promptScreened === true ? { promptScreened: true } : {}),
|
|
46
46
|
...(fields.projectContext !== undefined ? { projectContext: fields.projectContext } : {}),
|
|
47
47
|
...(fields.memorySeed !== undefined ? { memorySeed: fields.memorySeed } : {}),
|
|
48
|
+
...(fields.permissionModeWrite !== undefined ? { permissionModeWrite: fields.permissionModeWrite } : {}),
|
|
48
49
|
};
|
|
49
50
|
return sealCheckpointCtx(ctx, sealer, sessionId);
|
|
50
51
|
}
|
package/dist/http/dispatch.d.ts
CHANGED
|
@@ -43,9 +43,13 @@ export interface HealthDeploymentModsSection {
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function readCredentialOk(req: IncomingMessage, config: ServiceConfig): boolean;
|
|
45
45
|
/**
|
|
46
|
-
* `GET /health` 这次请求的**读者档**(`health-reader.ts`
|
|
47
|
-
* 没配服务凭据 ⇒ `open`(谓词的第一臂);配了而谓词认 ⇒ `credentialed`;配了而谓词不认 ⇒ `anonymous
|
|
46
|
+
* `GET /health` 这次请求的**读者档**(`health-reader.ts` 头注:四档与每档拿什么)—— 唯一判官。读的是上面那一只读路谓词与请求的通道事实(`request-channel.ts`),不另立第二份「凭据」读法:
|
|
47
|
+
* 没配服务凭据 ⇒ `open`(谓词的第一臂);配了而谓词认 ⇒ `credentialed`;配了而谓词不认 ⇒ 按通道穷举:经本机控制 socket ⇒ `engine-user`,经 TCP ⇒ `anonymous`
|
|
48
|
+
* (TCP 对端地址不读 —— 没有回环档)。`credentialed` 先于 `engine-user`:经 socket 带了凭据的读者归凭据档(两档投影相同,先后只影响「它证明了什么」)。
|
|
49
|
+
* 通道一臂只在这一面换档:读路谓词、全局凭据门与身份来源闭集都不读通道(通道不是凭据)。
|
|
48
50
|
* 调用点必须在鉴权桥预认证**之后**(已验登录令牌是谓词的一臂,读的是预认证挂上的身份)。
|
|
51
|
+
* **圈禁的身份不算读路凭据**(名册的非人档;`/health` 不声明权力,判官 `requestConfinedTier`):这次请求只按它出示的 service / trace 凭据分档 ——
|
|
52
|
+
* 与别的空间的登录令牌([ref])同一读法:探针面从不拒,身份不算,读者按没出示凭据分档(没出示 ⇒ 照上面按通道穷举:经本机控制 socket 仍是 `engine-user`,通道不是凭据、圈禁也不改它)。
|
|
49
53
|
*/
|
|
50
54
|
export declare function healthReaderOf(req: IncomingMessage, config: ServiceConfig): HealthReader;
|
|
51
55
|
export declare function createDispatch(ctx: {
|
package/dist/http/dispatch.js
CHANGED
|
@@ -6,16 +6,18 @@ import { publicStoreProbeError } from "../store-live-probe.js";
|
|
|
6
6
|
import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
|
|
7
7
|
import { admitBridgeIdentity, ssoForeignScope } from "../auth-bridge.js";
|
|
8
8
|
import { ROUTE_METHOD_ORDER } from "./route-ctx.js";
|
|
9
|
-
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath, isSubscriptionRequest } from "./route-table.js";
|
|
9
|
+
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath, isSubscriptionRequest, declaredPowerOf } from "./route-table.js";
|
|
10
10
|
import { sendJson, sendError, sendHttpError, isSseResponse, endSseWithError } from "./send.js";
|
|
11
11
|
import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal, billableRefusalOf, billableRefusalFrame } from "./readiness.js";
|
|
12
|
-
import { ENGINE_SIGNAL_KEY
|
|
12
|
+
import { ENGINE_SIGNAL_KEY } from "@sema-agent/settings-schema/fleet";
|
|
13
|
+
import { buildEngineSignal, fleetMembershipKeys, healthConfigLedgerKeys, memberLeaseOf, mintEngineSignal, restartActionable } from "../fleet/engine-signal.js";
|
|
13
14
|
import { buildHealthSelfReport } from "./health-self-report.js";
|
|
14
15
|
import { projectHealthBody } from "./health-reader.js";
|
|
16
|
+
import { requestChannelOf } from "./request-channel.js";
|
|
15
17
|
import { projectHealthStoreSection, unwiredStorePosture } from "../store-posture.js";
|
|
16
18
|
import { autoBackgroundOnTimeout as autoBackgroundOnTimeoutFact, autoBackgroundOnTimeoutWord, deploymentModPackages as deploymentModPackagesFact } from "../capability-facts.js";
|
|
17
19
|
import { hostBackgroundShellEnabled } from "../config.js";
|
|
18
|
-
import { actsForDeployment, admitRunIdentity, authorized, bearerPresentedButUnverified, devOpenDeployment, systemFor, gatedSubject, sendPrincipalRefusal, sendUnauthorized, traceTokenPresented, verifiedIdentitySource } from "./principal-gate.js";
|
|
20
|
+
import { actsForDeployment, admitRunIdentity, authorized, bearerPresentedButUnverified, devOpenDeployment, requestConfinedTier, systemFor, gatedSubject, sendPrincipalConfined, sendPrincipalRefusal, sendUnauthorized, traceTokenPresented, verifiedIdentitySource } from "./principal-gate.js";
|
|
19
21
|
import { hasServiceAuth, passesCredentialGate } from "../identity-source.js";
|
|
20
22
|
import { inflightTotal } from "../fleet/drain-entry.js";
|
|
21
23
|
const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
|
|
@@ -29,7 +31,21 @@ export function readCredentialOk(req, config) {
|
|
|
29
31
|
export function healthReaderOf(req, config) {
|
|
30
32
|
if (!hasServiceAuth(config))
|
|
31
33
|
return "open";
|
|
32
|
-
|
|
34
|
+
const confined = requestConfinedTier(req, config, declaredPowerOf("GET", "/health")) !== undefined;
|
|
35
|
+
const credentialed = confined ? systemFor(req, config) !== undefined || traceTokenPresented(req, config) : readCredentialOk(req, config);
|
|
36
|
+
if (credentialed)
|
|
37
|
+
return "credentialed";
|
|
38
|
+
const channel = requestChannelOf(req);
|
|
39
|
+
switch (channel) {
|
|
40
|
+
case "control-socket":
|
|
41
|
+
return "engine-user";
|
|
42
|
+
case "tcp":
|
|
43
|
+
return "anonymous";
|
|
44
|
+
default: {
|
|
45
|
+
const unknownChannel = channel;
|
|
46
|
+
throw new Error(`health reader: request channel ${String(unknownChannel)} has no reader tier`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
}
|
|
34
50
|
function credentialAttribution(req, credential, config) {
|
|
35
51
|
switch (credential) {
|
|
@@ -211,7 +227,6 @@ export function createDispatch(ctx) {
|
|
|
211
227
|
const fullBody = {
|
|
212
228
|
status: "ok",
|
|
213
229
|
...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
|
|
214
|
-
...healthDrainingKeys(signal),
|
|
215
230
|
...(() => {
|
|
216
231
|
const breakdown = deps.drainState?.inflightBreakdown?.();
|
|
217
232
|
return breakdown ? { inflight: inflightTotal(breakdown), inflightBreakdown: breakdown } : {};
|
|
@@ -317,6 +332,11 @@ export function createDispatch(ctx) {
|
|
|
317
332
|
sendError(res, 403, "auth.scope_mismatch", "forbidden — the sign-in token was issued for a different scope than the one this worker serves: send a sign-in token issued for this worker's scope, or send this request to a worker of your token's scope");
|
|
318
333
|
return;
|
|
319
334
|
}
|
|
335
|
+
const confined = requestConfinedTier(req, deps.config, declaredPowerOf(method, url));
|
|
336
|
+
if (confined !== undefined) {
|
|
337
|
+
sendPrincipalConfined(res, confined);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
320
340
|
if (await dispatchSegment(READ_CREDENTIAL_ROUTES, req, res, url, search, method, allow.pre, ctx, (r) => readCredentialOk(r, deps.config)))
|
|
321
341
|
return;
|
|
322
342
|
if (await dispatchSegment(PUBLIC_ROUTES, req, res, url, search, method, allow.pre, ctx, undefined, false))
|
|
@@ -113,7 +113,6 @@ function launchDurableRun(ctx, runStore, args, sessionId, taskId, trustedInterna
|
|
|
113
113
|
coordinator: deps.toolApproval,
|
|
114
114
|
streamApprovalOn: resolveStreamApprovalGate({
|
|
115
115
|
toolApprovalEnabled: true,
|
|
116
|
-
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
117
116
|
backend: deps.backend,
|
|
118
117
|
checkpointStore: deps.checkpointStore,
|
|
119
118
|
}).active,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `GET /health` 的**读者分档** ——
|
|
2
|
+
* `GET /health` 的**读者分档** —— 同一张面五种读者(k8s / Docker 探针、壳在带凭据之前读的自报位、嵌入宿主的引擎身份验证、同机附着方、持凭运维),
|
|
3
3
|
* 键集按**读者**而不是按「这个键敏不敏感」定。
|
|
4
4
|
*
|
|
5
|
-
* ##
|
|
6
|
-
*
|
|
5
|
+
* ## 律:证明只有两种,判官只有一只
|
|
6
|
+
* 一次请求的读者能向本服务证明的只有两件事 —— **凭据**(读路谓词 `dispatch.ts` 的 `readCredentialOk`:service token ∨ 只读 trace token ∨ 已验 principal)
|
|
7
|
+
* 与**通道**(请求从本机控制 socket 进来,`request-channel.ts`:能穿过数据根下私有目录的只有引擎的 OS 用户与 root)。TCP 对端地址**不是**证明 ——
|
|
8
|
+
* 它说的是包从哪只接口进来,不说包是谁发的(同机反代 / TCP 中继 / 共享绑址上更具体的回环绑定都让它失真),所以没有「回环档」,转发头也不读。
|
|
9
|
+
* 读者四档,答案只有一处(`dispatch.ts` 的 `healthReaderOf`;本文件只持有闭集与出口):
|
|
7
10
|
* · `open` —— 部署没配任何服务凭据(单机 / loopback 开发形;全局凭据门不武装):全键,字节同分档之前;
|
|
8
|
-
* · `credentialed` ——
|
|
9
|
-
* · `
|
|
11
|
+
* · `credentialed` —— 配了服务凭据,且这次请求出示了**读路谓词**认的凭据:全键;
|
|
12
|
+
* · `engine-user` —— 配了服务凭据、没出示凭据,但请求经**本机控制 socket** 到达:全键。它换来的只是这一面的自描述(本面按构造不带任何秘密),
|
|
13
|
+
* **不是凭据**:别的任何口照旧要凭据(通道不进读路谓词、全局门与身份来源闭集 —— 宿主车道上模型跑的命令也是引擎的 OS 用户);
|
|
14
|
+
* 这是一条显式的披露政策 —— 「引擎的 OS 用户有权读引擎的自描述」;
|
|
15
|
+
* · `anonymous` —— 配了服务凭据,这次请求既没出示凭据、也不是经控制 socket 来的:**只拿显式白名单** {@link ANONYMOUS_HEALTH_KEYS}。
|
|
10
16
|
* 白名单 = 存活位(`status`)+ 壳的 pre-credential 自报位三键(`auth` / `operator` / `center`,`health-self-report.ts` 铸;壳在带凭据**之前**要读的就是它们)。
|
|
11
17
|
* 其余的键 —— 现有的(`dataRoot` / `pid` / `configHash` / `sessionBackend` / `startedAt` / `port` / `version` / `engineSignal` …)与将来新长的 —— 对匿名读者一律不出:
|
|
12
18
|
* 这是**白名单**(挑出四键),不是黑名单(剥掉几键)—— 黑名单形下每个新键默认漏出,这一面又恒在凭据门之前、缺省全网卡监听(安全轴 fail-closed)。
|
|
@@ -14,14 +20,15 @@
|
|
|
14
20
|
* ## 谁读哪一档
|
|
15
21
|
* · 探针(k8s `httpGet` / Docker `HEALTHCHECK`):只要 HTTP 200 —— 匿名也是 200,存活位在白名单里;摘流语义的探针读 `/readyz`(不分档,体只有就绪位);
|
|
16
22
|
* · 壳的 pre-credential 读路:`auth` / `operator` / `center` 三键(7.121.0)—— 白名单;
|
|
17
|
-
* · 嵌入宿主的引擎身份验证(`pid` ∧ `dataRoot`,见 `test/health-identity-contract.test.ts`)
|
|
23
|
+
* · 嵌入宿主的引擎身份验证(`pid` ∧ `dataRoot`,见 `test/health-identity-contract.test.ts`):带部署的服务凭据读 ⇒ `credentialed`;经控制 socket 读 ⇒ `engine-user`;
|
|
18
24
|
* 没配服务凭据的嵌入形(桌面宿主自起、不配凭据)在 `open` 档,与改前逐字节同;
|
|
25
|
+
* · 同机附着方(没有令牌的壳 / 桌面宿主附着别的进程起的引擎):经控制 socket 读 ⇒ `engine-user`(读法见 USAGE「同机附着方怎么读身份」);
|
|
19
26
|
* · 持凭运维 / 编排器:`credentialed` 档,全键。
|
|
20
27
|
*
|
|
21
28
|
* 没有旋钮:一个放宽用的旋钮就是一把「把路径指纹重新开给匿名方」的钥匙,而本规则要的恰是它没有这把钥匙。
|
|
22
29
|
*/
|
|
23
|
-
/**
|
|
24
|
-
export declare const HEALTH_READERS: readonly ["open", "credentialed", "anonymous"];
|
|
30
|
+
/** 读者四档(闭集;顺序无语义)。 */
|
|
31
|
+
export declare const HEALTH_READERS: readonly ["open", "credentialed", "engine-user", "anonymous"];
|
|
25
32
|
export type HealthReader = (typeof HEALTH_READERS)[number];
|
|
26
33
|
/**
|
|
27
34
|
* 匿名读者的**显式白名单**:存活位 + 自报位三键。穷举 —— 加一个进来是一次有意的、要过评审的放宽(它会对没有凭据的任何人可见)。
|
|
@@ -29,7 +36,7 @@ export type HealthReader = (typeof HEALTH_READERS)[number];
|
|
|
29
36
|
*/
|
|
30
37
|
export declare const ANONYMOUS_HEALTH_KEYS: readonly ["status", "auth", "operator", "center"];
|
|
31
38
|
/**
|
|
32
|
-
* 键集判官 —— `/health` 体按读者出口的**唯一**一道:`open` / `credentialed` 原样,`anonymous` 只挑白名单。
|
|
39
|
+
* 键集判官 —— `/health` 体按读者出口的**唯一**一道:`open` / `credentialed` / `engine-user` 原样,`anonymous` 只挑白名单。
|
|
33
40
|
* 入参是按全键铸好的体(铸点不分读者:一个事实一个铸点,读者分档只在出口);返回新对象,不改入参。
|
|
34
41
|
*/
|
|
35
42
|
export declare function projectHealthBody(reader: HealthReader, body: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export const HEALTH_READERS = ["open", "credentialed", "anonymous"];
|
|
1
|
+
export const HEALTH_READERS = ["open", "credentialed", "engine-user", "anonymous"];
|
|
2
2
|
export const ANONYMOUS_HEALTH_KEYS = ["status", "auth", "operator", "center"];
|
|
3
3
|
export function projectHealthBody(reader, body) {
|
|
4
4
|
switch (reader) {
|
|
5
5
|
case "open":
|
|
6
6
|
case "credentialed":
|
|
7
|
+
case "engine-user":
|
|
7
8
|
return body;
|
|
8
9
|
case "anonymous": {
|
|
9
10
|
const closed = {};
|
|
@@ -30,7 +30,7 @@ export function buildHealthSelfReport(deps) {
|
|
|
30
30
|
const required = kinds.includes("service_token");
|
|
31
31
|
return {
|
|
32
32
|
auth: { required, kinds, identityAttach: kinds.some(carriesCallerIdentity) ? "available" : "unavailable", serviceTokenActsForDeployment: required && serviceTokenDeclaredActsForDeployment(config) },
|
|
33
|
-
operator: operatorLane(deps).available ? "present" : "absent",
|
|
33
|
+
operator: operatorLane(deps, "operate").available ? "present" : "absent",
|
|
34
34
|
center: {
|
|
35
35
|
...(issuer !== undefined ? { issuer: buildDisplaySafeUrl(issuer) } : {}),
|
|
36
36
|
...(webBaseUrl !== undefined ? { webBaseUrl: buildDisplaySafeUrl(webBaseUrl) } : {}),
|
|
@@ -32,7 +32,7 @@ import { type SessionFace } from "../session-incarnation.js";
|
|
|
32
32
|
import { type Admitted, type Caller, type FaceId, type ListFaceId, type ListFilter, type OwnedReading, type Qual, type RunFaceId, type SessionFaceId, type WorkflowFaceId } from "../owner-faces.js";
|
|
33
33
|
import { type Person, type Subject } from "../owner-brands.js";
|
|
34
34
|
import { type OwnerAwareSessionStore, type SessionListItem } from "../security.js";
|
|
35
|
-
import {
|
|
35
|
+
import type { OperatorRoster } from "../operator-roster.js";
|
|
36
36
|
import { type RouteDeps } from "./route-ctx.js";
|
|
37
37
|
import type { FlatServiceDeps } from "./server.js";
|
|
38
38
|
import type { AgentRosterRouteId } from "./routes/agents-roster.js";
|
|
@@ -96,9 +96,8 @@ export declare const ROUTE_FACES: {
|
|
|
96
96
|
readonly "run-task-stop": "run.supervise";
|
|
97
97
|
readonly "elicitation-respond": null;
|
|
98
98
|
readonly "question-respond": null;
|
|
99
|
-
readonly "tool-approval-respond": null;
|
|
100
|
-
readonly "tool-approval-read": null;
|
|
101
99
|
readonly "task-ask-decision": null;
|
|
100
|
+
readonly "task-ask-read": null;
|
|
102
101
|
readonly "run-permission-mode": "run.supervise";
|
|
103
102
|
readonly "run-memory-capture-optout": "run.drive";
|
|
104
103
|
readonly "session-init": null;
|
|
@@ -16,7 +16,7 @@ export function callerOf(req, w) {
|
|
|
16
16
|
const subject = gatedSubject(req, w.config);
|
|
17
17
|
return {
|
|
18
18
|
subject,
|
|
19
|
-
operator: explicitOperatorOk(subject === undefined ? undefined : personOf(subject), w.config),
|
|
19
|
+
operator: explicitOperatorOk(subject === undefined ? undefined : personOf(subject), w.config, "operate"),
|
|
20
20
|
fleet: actsForDeployment(req, w.config, () => subject),
|
|
21
21
|
traceRead: traceTokenPresented(req, w.config),
|
|
22
22
|
principalRequired: DEPLOYMENT_SHAPE_PREDICATES.principalRequired.holds(w.config),
|
|
@@ -24,7 +24,7 @@ export function callerOf(req, w) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
export function callerOfDecision(base, decider, roster) {
|
|
27
|
-
return { ...base, subject: deciderSubjectOf(base.subject, decider), operator: explicitOperatorOk(decider, roster), fleet: undefined, traceRead: false };
|
|
27
|
+
return { ...base, subject: deciderSubjectOf(base.subject, decider), operator: explicitOperatorOk(decider, roster, "operate"), fleet: undefined, traceRead: false };
|
|
28
28
|
}
|
|
29
29
|
export function deciderSubjectOf(requestSubject, decider) {
|
|
30
30
|
return decider === undefined ? undefined : subjectOf(decider, requestSubject === undefined ? undefined : partsOf(requestSubject).scenario);
|
|
@@ -47,9 +47,8 @@ export const ROUTE_FACES = {
|
|
|
47
47
|
"run-task-stop": "run.supervise",
|
|
48
48
|
"elicitation-respond": null,
|
|
49
49
|
"question-respond": null,
|
|
50
|
-
"tool-approval-respond": null,
|
|
51
|
-
"tool-approval-read": null,
|
|
52
50
|
"task-ask-decision": null,
|
|
51
|
+
"task-ask-read": null,
|
|
53
52
|
"run-permission-mode": "run.supervise",
|
|
54
53
|
"run-memory-capture-optout": "run.drive",
|
|
55
54
|
"session-init": null,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { ServiceConfig } from "../config-types.js";
|
|
3
|
+
import { type NonPersonTier, type OperatorPower, type OperatorRoster } from "../operator-roster.js";
|
|
3
4
|
import { type Person, type Subject } from "../owner-brands.js";
|
|
4
|
-
import type
|
|
5
|
+
import { type Caller } from "../owner-faces.js";
|
|
5
6
|
import { type IdentitySource, type RunIdentity } from "../identity-source.js";
|
|
6
7
|
import type { Logger } from "../observability/logger.js";
|
|
7
8
|
/** Coerce a possibly-repeated request header to a single string (or undefined). */
|
|
@@ -126,23 +127,50 @@ export declare function actsForDeployment(req: IncomingMessage, config: ServiceC
|
|
|
126
127
|
*/
|
|
127
128
|
export declare function serviceTokenDeclaredActsForDeployment(config: Pick<ServiceConfig, "serviceTokenActsForDeployment">): boolean;
|
|
128
129
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*/
|
|
132
|
-
export type OperatorRoster = Pick<ServiceConfig, "operatorPrincipals" | "operatorSingleUser">;
|
|
133
|
-
/**
|
|
134
|
-
* 🔴 **「谁是 operator」的唯一读法**([ref],[ref] ① 裁 A;IMAGE-API-DESIGN.md §P2.4a MUST-FIX 的那一只门)。
|
|
130
|
+
* 🔴 **「这位验过的人在这台部署上持不持这只运维权力」的唯一读法**([ref],[ref] ① 裁 A;IMAGE-API-DESIGN.md §P2.4a MUST-FIX 的那一只门)。
|
|
131
|
+
* 档表与人 → 档的读法住在叶子 `operator-roster.ts`(`ROSTER_TIERS` / `rosterTierOf`);本函数 = 「人落在某档 ∧ 那一档持 `power`」。
|
|
135
132
|
* · 名单为空 ⇒ **没人是**(没有任何形把「空名单」读成「人人是」—— 那条读法 `isOperator` 已删);
|
|
136
|
-
* · 名单非空 ⇒
|
|
137
|
-
* · 单用户形显式声明 `OPERATOR_SINGLE_USER=true` ⇒
|
|
138
|
-
* 凭据门 —— 没配凭据的部署上,能连上端口的都是)
|
|
133
|
+
* · 名单非空 ⇒ 验过的人在名单里才是;全运维档(`OPERATOR_PRINCIPALS`)持每一只权力,排空档(`OPERATOR_DRAIN_PRINCIPALS`)只持 `drain`;
|
|
134
|
+
* · 单用户形显式声明 `OPERATOR_SINGLE_USER=true` ⇒ 这台部署的**每个调用方**落全运维档(单用户形只有一位用户,能到这台部署的就是他;边界是部署自己的
|
|
135
|
+
* 凭据门 —— 没配凭据的部署上,能连上端口的都是)。声明只在单用户形、且各档名单都空时成立(起服不变量)。
|
|
136
|
+
* `power` 必填、零缺省:运维面的门与放宽位传 `"operate"`,排空口传它声明行的 `power`(`routes/admin-drain.ts`)。
|
|
139
137
|
* `person` 必须是验过的人(`gatedPerson` / `personOf(主体)`),绝不是自述头,也不是带场景的主体(名册登记的是人)。全部运维面(审批队列、steer / interrupt / wake 的属主门旁路、记忆运维口、bake 门……)读这一只 —— 只答授权;
|
|
140
138
|
* 送话口的文本框架(`trusted`)从不读它(调用方的话恒 `trusted: false`,见 `routes/runs.ts` steer 口的注);
|
|
141
139
|
* 🔴 SINGLE mint point (E1):不要再造兄弟。
|
|
142
140
|
*/
|
|
143
|
-
export declare function explicitOperatorOk(person: Person | undefined, roster: OperatorRoster): boolean;
|
|
144
|
-
/**
|
|
145
|
-
export declare function operatorRosterPresent(roster: OperatorRoster): boolean;
|
|
141
|
+
export declare function explicitOperatorOk(person: Person | undefined, roster: OperatorRoster, power: OperatorPower): boolean;
|
|
142
|
+
/** 「这台部署上**有没有**人能持这只权力」的唯一读法(持它的某档名单非空 ∨ 单用户声明在场)—— 能力事实 `operatorLane` 读这一只(整理阀门的可达性告警随本条退役)。单用户形上,装了运维面的进程里它恒真(`operator-declaration-required`:服务进程起服第 1 段、`createHttpServer` 对不声明的单用户形拒),多租形上持它的各档名单都空 ⇒ 假。 */
|
|
143
|
+
export declare function operatorRosterPresent(roster: OperatorRoster, power: OperatorPower): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* 🔴 **圈禁的唯一判官**(三个读点,同一只判:请求的人 —— {@link requestConfinedTier},分派层整次请求判一次;决定证明推出的决定者 —— 直连门决定口验过证明之后,
|
|
146
|
+
* `routes/approvals-assistant.ts` 的 `admitDirectDoorDecider`;设备行的属主 —— {@link deviceOwnerConfinedTier},设备 hello 准入,`device-ws-hub.ts`)。`person` 落在**非人档** ∧ 这一条路由没声明那一档持的权力(`power` = 路由声明行的 `power` 列,缺席 = 没声明)
|
|
147
|
+
* ⇒ 答那一档(调用方 403 `auth.principal_confined`,{@link sendPrincipalConfined});人档 / 不在名册 / 路由声明了它持的权力 ⇒ `undefined`(照常往下判)。
|
|
148
|
+
* 不是第二套鉴权:身份读法、名册来源都不新;它答的是「这只验过的身份在本请求上能不能用」——与 [ref] `auth.scope_mismatch` 同一位置的第二个原因。
|
|
149
|
+
*/
|
|
150
|
+
export declare function confinedTierOf(person: Person | undefined, roster: OperatorRoster, power: OperatorPower | undefined): NonPersonTier | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* 请求读点:这次请求的人在本路由上被不被圈禁。**只在名册里有非人档的名字时读人**(`rosterHasNonPersonNames`;没声明的部署零新读、逐字节同今天)。
|
|
153
|
+
* 读的是这次请求里**任一只验得过的人**(`verifiedPerson`:三来源按身份边界的序),**不**因同一请求另带了一枚验不过的 Bearer 而读成「没有人」——
|
|
154
|
+
* 那条律(`gatedSubject`)只收窄身份、从不授权;圈禁是收窄的另一面,请求里只要出示了一只被圈禁的身份就判它(读路凭据谓词照样会认那只身份,
|
|
155
|
+
* 例如直连门 principal 令牌 + 坏 Bearer 的 `/health`,所以这里不能比它少认)。**不判场景头**(不把「REQUIRE 401 先于场景头 400」的既有序提前改掉)。
|
|
156
|
+
* 读人只在人名坏形时抛(`assertPrincipalShape` 的 400):非人档的名字由起服 / 装配不变量 `operator-roster-well-formed` 保证都是合形的主体名,坏形的串
|
|
157
|
+
* 永远对不上任何一个 ⇒ 本请求没有可圈禁的身份 ⇒ 不判(下游每一次读人都抛同一只、同一处答 400)。这不是放宽臂:名单里的人永远读得出来。
|
|
158
|
+
*/
|
|
159
|
+
export declare function requestConfinedTier(req: IncomingMessage, config: ServiceConfig, power: OperatorPower | undefined): NonPersonTier | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* 设备读点:设备行的属主此刻被不被圈禁。设备车道不经 HTTP(`GET /v1/device/ws` 凭设备密钥对握手,不带任何一枚人的凭据),所以请求读点看不到它;设备行按**属主**绑定,
|
|
162
|
+
* 登记那一刻的属主是验过的人 —— `http/execution-context.ts` 的 `deviceOwnerDomainOf` 是复合身份 `{ tenant, subject }` 的唯一铸点,`subject` 格逐字节取自 `gatedPerson` ——
|
|
163
|
+
* 于是行里存的就是一只持久的属主串,按恢复管线(`recoveredSubject` → `personOf`;与组织信封的发件人同一条)读回那位人,再交同一只判官。设备车道上没有任何路由声明过权力
|
|
164
|
+
* (`power` 缺席):落在非人档的属主在这条车道上整个不算数 —— 它登记的设备不许连上、不许为它执行指令(HTTP 面同一位置已 403)。
|
|
165
|
+
* 读的是名册**现态**(`roster` 每次现读,不拍快照、不把「登记时在哪一档」记在设备行里)。名册只来自 env、起服冻结 ⇒ 进程内连着的设备不会被挪档;挪档后的重启,新进程的每一次 hello 按新名册判。
|
|
166
|
+
*/
|
|
167
|
+
export declare function deviceOwnerConfinedTier(owner: {
|
|
168
|
+
readonly subject: string;
|
|
169
|
+
}, roster: OperatorRoster): NonPersonTier | undefined;
|
|
170
|
+
/** 圈禁拒句(一档一句,模块级常量;冻结门按常量解引用逐字钉)。不回显主体名;点名名单所在的 env 与这一档能用的那一条路由。 */
|
|
171
|
+
export declare const PRINCIPAL_CONFINED_DRAIN = "forbidden \u2014 this caller is a drain-only service principal on this deployment (it is named in OPERATOR_DRAIN_PRINCIPALS), and that identity counts only on POST /v1/admin/drain: every other endpoint refuses it; use an identity that is not on that list for anything else";
|
|
172
|
+
/** `403 auth.principal_confined` 的唯一铸点(一档一句,穷举 switch:加一只非人档不接句子 = 编译红)。 */
|
|
173
|
+
export declare function sendPrincipalConfined(res: ServerResponse, tier: NonPersonTier): void;
|
|
146
174
|
/**
|
|
147
175
|
* 起服一行:部署声明 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` ⇒ `service_token_posture { actsForDeployment: false }`(info);缺省不打。
|
|
148
176
|
* 声明形让运维面板开位当天成片 401,而「位关了」与「token 错了」在 wire 上同体(反探测律),这一行是起服时唯一一处说清这台部署选了哪一读法的地方。
|
|
@@ -154,14 +182,14 @@ export declare function buildServiceTokenPostureNotice(config: Pick<ServiceConfi
|
|
|
154
182
|
actsForDeployment: false;
|
|
155
183
|
};
|
|
156
184
|
} | undefined;
|
|
157
|
-
/** operator
|
|
158
|
-
* 身份都打不通 = 这台部署上不可用 ⇒ 501,不是鉴权拒(403 只留给「名单非空而你不在名单上」)
|
|
159
|
-
export declare function sendOperatorPrincipalsRequired(res: ServerResponse): void;
|
|
185
|
+
/** operator 面族在「持这只权力的名单都空」这一形上的**唯一**拒发口:事实 `operatorLane(d, power)`(`capability-facts.ts`)为假 ⇒ 这一面对任何
|
|
186
|
+
* 身份都打不通 = 这台部署上不可用 ⇒ 501,不是鉴权拒(403 只留给「名单非空而你不在名单上」)。能力位读的是同一只事实。一只权力一句(穷举 switch)。 */
|
|
187
|
+
export declare function sendOperatorPrincipalsRequired(res: ServerResponse, power: OperatorPower): void;
|
|
160
188
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
189
|
+
* 「这台部署上没有人持这只权力」时**唯一**的出路句 —— 与 {@link operatorRosterPresent} 同一组旋钮(档表里持 `power` 的各档名单 + 单用户声明),所以句子不比事实少说一条出路:
|
|
190
|
+
* 持它的每一档名单(按档表的规范序列出;`operate` 只有 `OPERATOR_PRINCIPALS`,`drain` 另有 `OPERATOR_DRAIN_PRINCIPALS`),或单用户形的显式声明 `OPERATOR_SINGLE_USER=true`
|
|
191
|
+
* (只在 `REQUIRE_PRINCIPAL` 未开且各档名单都空时成立,否则起服拒 —— 句子把它限定在单用户部署上)。拒发口 {@link sendOperatorPrincipalsRequired}、能力事实闭集 `operator_principals_empty` 的义、
|
|
192
|
+
* 记忆整理阀门的起服告警都说这一句,别处不再各写一份。
|
|
165
193
|
*/
|
|
166
|
-
export declare function buildOperatorRosterWayOut(): string;
|
|
194
|
+
export declare function buildOperatorRosterWayOut(power: OperatorPower): string;
|
|
167
195
|
//# sourceMappingURL=principal-gate.d.ts.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
|
|
3
|
-
import { PRINCIPAL_TOKEN_HEADER, verifiedPerson, verifiedSubject, scenarioHeaderOf, scenarioWithoutPrincipalError, ssoVerifiedPrincipal, ssoVerifiedPrincipalInc, ssoVerifiedScope } from "../security.js";
|
|
3
|
+
import { HttpError, PRINCIPAL_TOKEN_HEADER, verifiedPerson, verifiedSubject, scenarioHeaderOf, scenarioWithoutPrincipalError, ssoVerifiedPrincipal, ssoVerifiedPrincipalInc, ssoVerifiedScope } from "../security.js";
|
|
4
|
+
import { ROSTER_TIERS, ROSTER_TIER_ORDER, rosterHasNonPersonNames, rosterNamesOf, rosterTierOf, tierHolds, tierIsPerson } from "../operator-roster.js";
|
|
4
5
|
import { personOf } from "../owner-brands.js";
|
|
6
|
+
import { recoveredSubject } from "../owner-faces.js";
|
|
5
7
|
import { IDENTITY_SOURCES, runIdentityClaimOverflow } from "../identity-source.js";
|
|
6
8
|
import { sendError } from "./send.js";
|
|
7
9
|
import { canonicalAddress } from "../net-address.js";
|
|
@@ -159,23 +161,71 @@ export function actsForDeployment(req, config, subject) {
|
|
|
159
161
|
export function serviceTokenDeclaredActsForDeployment(config) {
|
|
160
162
|
return config.serviceTokenActsForDeployment === true;
|
|
161
163
|
}
|
|
162
|
-
export function explicitOperatorOk(person, roster) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return roster.operatorPrincipals.length > 0 && person !== undefined && roster.operatorPrincipals.includes(person);
|
|
164
|
+
export function explicitOperatorOk(person, roster, power) {
|
|
165
|
+
const tier = rosterTierOf(person, roster);
|
|
166
|
+
return tier !== undefined && tierHolds(tier, power);
|
|
166
167
|
}
|
|
167
|
-
export function operatorRosterPresent(roster) {
|
|
168
|
-
return roster.operatorSingleUser === true || roster.
|
|
168
|
+
export function operatorRosterPresent(roster, power) {
|
|
169
|
+
return roster.operatorSingleUser === true || ROSTER_TIER_ORDER.some((tier) => tierHolds(tier, power) && rosterNamesOf(tier, roster).length > 0);
|
|
170
|
+
}
|
|
171
|
+
export function confinedTierOf(person, roster, power) {
|
|
172
|
+
const tier = rosterTierOf(person, roster);
|
|
173
|
+
if (tier === undefined || tierIsPerson(tier))
|
|
174
|
+
return undefined;
|
|
175
|
+
if (power !== undefined && tierHolds(tier, power))
|
|
176
|
+
return undefined;
|
|
177
|
+
return tier;
|
|
178
|
+
}
|
|
179
|
+
export function requestConfinedTier(req, config, power) {
|
|
180
|
+
if (!rosterHasNonPersonNames(config))
|
|
181
|
+
return undefined;
|
|
182
|
+
let person;
|
|
183
|
+
try {
|
|
184
|
+
person = verifiedPerson(req, config);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
if (e instanceof HttpError)
|
|
188
|
+
return undefined;
|
|
189
|
+
throw e;
|
|
190
|
+
}
|
|
191
|
+
return confinedTierOf(person, config, power);
|
|
192
|
+
}
|
|
193
|
+
export function deviceOwnerConfinedTier(owner, roster) {
|
|
194
|
+
return confinedTierOf(personOf(recoveredSubject(owner.subject)), roster, undefined);
|
|
195
|
+
}
|
|
196
|
+
export const PRINCIPAL_CONFINED_DRAIN = "forbidden — this caller is a drain-only service principal on this deployment (it is named in OPERATOR_DRAIN_PRINCIPALS), and that identity counts only on POST /v1/admin/drain: every other endpoint refuses it; use an identity that is not on that list for anything else";
|
|
197
|
+
export function sendPrincipalConfined(res, tier) {
|
|
198
|
+
switch (tier) {
|
|
199
|
+
case "drain":
|
|
200
|
+
sendError(res, 403, "auth.principal_confined", PRINCIPAL_CONFINED_DRAIN);
|
|
201
|
+
return;
|
|
202
|
+
default: {
|
|
203
|
+
const unknownTier = tier;
|
|
204
|
+
throw new Error(`sendPrincipalConfined: unknown confined tier ${String(unknownTier)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
169
207
|
}
|
|
170
208
|
export function buildServiceTokenPostureNotice(config) {
|
|
171
209
|
if (serviceTokenDeclaredActsForDeployment(config))
|
|
172
210
|
return undefined;
|
|
173
211
|
return { event: "service_token_posture", fields: { actsForDeployment: false } };
|
|
174
212
|
}
|
|
175
|
-
export function sendOperatorPrincipalsRequired(res) {
|
|
176
|
-
|
|
213
|
+
export function sendOperatorPrincipalsRequired(res, power) {
|
|
214
|
+
switch (power) {
|
|
215
|
+
case "operate":
|
|
216
|
+
sendError(res, 501, "capability.operator_principals_required", `this endpoint is operator-only and this deployment has no operator roster (OPERATOR_PRINCIPALS is empty and OPERATOR_SINGLE_USER is not declared), so no caller can be an operator; ${buildOperatorRosterWayOut(power)}`);
|
|
217
|
+
return;
|
|
218
|
+
case "drain":
|
|
219
|
+
sendError(res, 501, "capability.operator_principals_required", `this endpoint is open to this deployment's operators and its drain-only service principals, and it names neither (OPERATOR_PRINCIPALS and OPERATOR_DRAIN_PRINCIPALS are empty and OPERATOR_SINGLE_USER is not declared), so no caller can call it; ${buildOperatorRosterWayOut(power)}`);
|
|
220
|
+
return;
|
|
221
|
+
default: {
|
|
222
|
+
const unknownPower = power;
|
|
223
|
+
throw new Error(`sendOperatorPrincipalsRequired: unknown power ${String(unknownPower)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
177
226
|
}
|
|
178
|
-
export function buildOperatorRosterWayOut() {
|
|
179
|
-
|
|
227
|
+
export function buildOperatorRosterWayOut(power) {
|
|
228
|
+
const rosters = ROSTER_TIER_ORDER.filter((tier) => tierHolds(tier, power)).map((tier) => `${ROSTER_TIERS[tier].env} with the named ${ROSTER_TIERS[tier].holders}`);
|
|
229
|
+
return `configure ${rosters.join(" or ")}, or — on a single-user deployment (REQUIRE_PRINCIPAL off) — declare OPERATOR_SINGLE_USER=true`;
|
|
180
230
|
}
|
|
181
231
|
//# sourceMappingURL=principal-gate.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **通道事实**(纯叶,只依赖 `node:http` 的型):一条请求从本服务的哪一只监听口进来。
|
|
3
|
+
*
|
|
4
|
+
* ## 律(一条)
|
|
5
|
+
* 闭集 {@link REQUEST_CHANNELS} = `tcp`(TCP 监听口)· `control-socket`(本机控制 socket,`control-socket.ts`)。
|
|
6
|
+
* 唯一写者 = 控制 socket 那只 `http.Server` 的请求监听器({@link markControlSocketRequest},`http/server.ts` 里恰一处调用);唯一读法 = {@link requestChannelOf}。
|
|
7
|
+
* **缺标 ⇒ `tcp`**(fail-closed):没被那只监听器标过的请求一律按 TCP 读。不按 `req.socket.remoteAddress` 缺席反推 —— TCP 连接断开之后它同样缺席,反推形是 fail-open。
|
|
8
|
+
*
|
|
9
|
+
* ## 通道换来什么(不换来什么)
|
|
10
|
+
* 控制 socket 证明的是「对端是引擎的 OS 用户(或 root)」—— 能穿过私有目录的只有他们。宿主车道上模型跑的命令也是这个用户,所以**通道不是凭据**:
|
|
11
|
+
* 它不进读路谓词(`dispatch.ts` 的 `readCredentialOk`)、不进全局凭据门、不进身份来源闭集(`identity-source.ts` 的 `IDENTITY_SOURCES`);
|
|
12
|
+
* 它只换来 `/health` 的读者档 `engine-user`(引擎自描述的全键,`health-reader.ts`)、诊断面远程来源门的「本机」一读、限流的地址回落键。
|
|
13
|
+
* 读它的每一处都**穷举**本闭集(`switch` + `never`),没有一处写「等于某一词」的比较 —— 加一种通道不接 ⇒ 编译红,而不是默认得到某一档。
|
|
14
|
+
*/
|
|
15
|
+
import type { IncomingMessage } from "node:http";
|
|
16
|
+
/** 请求通道的闭集(顺序无语义)。 */
|
|
17
|
+
export declare const REQUEST_CHANNELS: readonly ["tcp", "control-socket"];
|
|
18
|
+
export type RequestChannel = (typeof REQUEST_CHANNELS)[number];
|
|
19
|
+
/** **唯一写者**:控制 socket 那只 `http.Server` 的请求监听器在把请求交给共享监听器之前调一次。别处调用 = 把 TCP 请求读成本机用户(源码门钉调用点恰一处)。 */
|
|
20
|
+
export declare function markControlSocketRequest(req: IncomingMessage): void;
|
|
21
|
+
/** 这条请求从哪一只监听口进来 —— 唯一读法;缺标 ⇒ `tcp`。 */
|
|
22
|
+
export declare function requestChannelOf(req: IncomingMessage): RequestChannel;
|
|
23
|
+
//# sourceMappingURL=request-channel.d.ts.map
|