@sema-agent/server 7.125.1 → 7.127.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 +229 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +346 -145
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +57 -0
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +32 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -2
- package/deploy/sema-up/chart/templates/server.yaml +2 -1
- package/deploy/sema-up/chart/values.yaml +22 -0
- package/deploy/sema-up/cluster-up.sh +10 -10
- package/deploy/sema-up/kube-up.sh +20 -13
- package/deploy/sema-up/lib/dataroot.sh +1 -1
- package/deploy/sema-up/lib/envf.sh +5 -5
- package/deploy/sema-up/lib/flagshape.sh +5 -5
- package/deploy/sema-up/lib/lanes.py +1 -1
- package/deploy/sema-up/lib/node-addrs.sh +42 -0
- package/deploy/sema-up/lib/parse-machines.py +2 -2
- package/deploy/sema-up/lib/shell-access.sh +1 -1
- package/deploy/sema-up/preflight.sh +4 -4
- package/deploy/sema-up/sema-up.sh +33 -33
- package/deploy/sema-up/smoke.sh +3 -3
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +26 -3
- package/dist/approval-card.d.ts +20 -4
- package/dist/approval-content-kind.d.ts +38 -22
- package/dist/approval-content-kind.js +20 -4
- package/dist/audit.js +3 -0
- package/dist/bench/s1/arms.d.ts +9 -37
- package/dist/bench/s1/arms.js +4 -61
- package/dist/bench/s1/live-deps.d.ts +4 -10
- package/dist/bench/s1/live-deps.js +1 -222
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/reviewer.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +5 -7
- package/dist/bench/s1/row.js +0 -2
- package/dist/bench/s1/run-firm.d.ts +2 -2
- package/dist/bench/s1/run-firm.js +1 -4
- package/dist/bench/s1/runner-ctx.d.ts +17 -29
- package/dist/bench/s1/runner-ctx.js +3 -7
- package/dist/bench/s1/tasks.d.ts +9 -46
- package/dist/bench/s1/tasks.js +4 -63
- package/dist/boot/execution-env.d.ts +38 -26
- package/dist/boot/execution-env.js +87 -41
- package/dist/boot/leader.d.ts +2 -1
- package/dist/boot/memory-boundary.d.ts +1 -1
- package/dist/boot/memory-consolidation.d.ts +37 -3
- package/dist/boot/memory-consolidation.js +18 -2
- package/dist/boot/parked-revive-gate.d.ts +0 -3
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.d.ts +0 -2
- package/dist/boot/resolve-spec.js +12 -17
- package/dist/boot/retention-lane.d.ts +12 -1
- package/dist/boot/retention-lane.js +2 -0
- package/dist/boot/runner-deps.d.ts +7 -6
- package/dist/boot/runner-deps.js +5 -16
- package/dist/boot/session-faces.d.ts +7 -2
- package/dist/boot/session-faces.js +37 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -0
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-05-execution-env.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.js +7 -4
- package/dist/boot/stage-06-runners.d.ts +2 -3
- package/dist/boot/stage-06-runners.js +13 -9
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +3 -4
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +4 -3
- package/dist/boot/web-admin.d.ts +5 -0
- package/dist/boot/web-admin.js +28 -14
- package/dist/brain.d.ts +2 -2
- package/dist/brain.js +1 -1
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +27 -22
- package/dist/capabilities/memory-notice.js +15 -17
- package/dist/capabilities/scenarios.d.ts +9 -14
- package/dist/capabilities/scenarios.js +5 -50
- package/dist/capability-facts.d.ts +22 -11
- package/dist/capability-facts.js +13 -6
- package/dist/config-catalog.js +14 -5
- package/dist/config-provider.d.ts +5 -1
- package/dist/config-provider.js +23 -4
- package/dist/config-types.d.ts +19 -2
- package/dist/config.js +41 -6
- package/dist/deployment-governance.d.ts +19 -21
- package/dist/deployment-governance.js +7 -8
- package/dist/env-name-allowlist-knobs.d.ts +5 -5
- package/dist/env-name-allowlist-knobs.js +1 -0
- package/dist/execution-lane-caps.d.ts +12 -0
- package/dist/execution-lane-caps.js +18 -4
- package/dist/fleet/subagent-tail-bus.js +15 -5
- package/dist/hooks/hook-runner.d.ts +84 -30
- package/dist/hooks/hook-runner.js +74 -41
- package/dist/http/admission.js +17 -10
- package/dist/http/durable-run-leg.d.ts +5 -4
- package/dist/http/durable-run-leg.js +2 -4
- package/dist/http/idempotency.d.ts +28 -6
- package/dist/http/idempotency.js +10 -2
- package/dist/http/principal-gate.d.ts +14 -0
- package/dist/http/principal-gate.js +7 -0
- package/dist/http/request-guards.d.ts +2 -1
- package/dist/http/request-guards.js +14 -14
- package/dist/http/resume-legs.d.ts +2 -2
- package/dist/http/resume-legs.js +20 -10
- package/dist/http/route-ctx.d.ts +15 -7
- package/dist/http/routes/a2a-serve.js +11 -7
- package/dist/http/routes/approvals-assistant.js +18 -3
- package/dist/http/routes/background.d.ts +1 -1
- package/dist/http/routes/background.js +1 -1
- package/dist/http/routes/capabilities.js +10 -2
- package/dist/http/routes/mcp-reconnect.js +1 -1
- package/dist/http/routes/runs.d.ts +4 -3
- package/dist/http/routes/runs.js +35 -14
- package/dist/http/routes/session-sync.js +14 -11
- package/dist/http/routes/sessions.js +9 -9
- package/dist/http/routes/side-query.js +6 -19
- package/dist/http/routes/tasks.js +29 -19
- package/dist/http/routes/workflows.js +8 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +3 -5
- package/dist/http/submit-replay.d.ts +15 -8
- package/dist/http/submit-replay.js +6 -0
- package/dist/http/wire-types.d.ts +7 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal-run-env.d.ts +30 -0
- package/dist/internal-run-env.js +11 -0
- package/dist/leader/wire.js +3 -2
- package/dist/lsp/manager.js +1 -1
- package/dist/memory-scope.d.ts +19 -7
- package/dist/memory-scope.js +25 -8
- package/dist/observability/fail-open.d.ts +31 -3
- package/dist/observability/fail-open.js +31 -3
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/metrics.js +1 -4
- package/dist/observability/run-terminal.js +72 -4
- package/dist/permission-rule-vocab.d.ts +3 -2
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plan-cache-probe.d.ts +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/caching-session-store.d.ts +42 -22
- package/dist/plugins/caching-session-store.js +19 -9
- package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
- package/dist/plugins/checkpoint-store-sql.js +57 -11
- package/dist/plugins/file-run-store.d.ts +25 -21
- package/dist/plugins/file-run-store.js +70 -34
- package/dist/plugins/k8s-exec-protocol.js +3 -0
- package/dist/plugins/local-checkpoint-store.d.ts +8 -5
- package/dist/plugins/local-checkpoint-store.js +5 -15
- package/dist/plugins/local-session-store.d.ts +66 -7
- package/dist/plugins/local-session-store.js +168 -101
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
- package/dist/plugins/memory-run-store.d.ts +19 -3
- package/dist/plugins/memory-run-store.js +38 -3
- package/dist/plugins/one-time-migrations.d.ts +165 -39
- package/dist/plugins/one-time-migrations.js +51 -144
- package/dist/plugins/pg-pool.js +11 -1
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +9 -5
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +1 -0
- package/dist/plugins/remote-env-e2b.d.ts +4 -0
- package/dist/plugins/remote-env-e2b.js +2 -1
- package/dist/plugins/remote-env-host.d.ts +23 -1
- package/dist/plugins/remote-env-host.js +20 -2
- package/dist/plugins/remote-env-k8s.d.ts +264 -41
- package/dist/plugins/remote-env-k8s.js +551 -82
- package/dist/plugins/remote-env-local-docker.d.ts +4 -0
- package/dist/plugins/remote-env-local-docker.js +2 -1
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/retention-store-sql.d.ts +4 -1
- package/dist/plugins/retention-store-sql.js +11 -9
- package/dist/plugins/run-store-sql.d.ts +44 -6
- package/dist/plugins/run-store-sql.js +92 -27
- package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
- package/dist/plugins/session-store.d.ts +4 -3
- package/dist/plugins/session-store.js +1 -1
- package/dist/plugins/sql-driver.d.ts +2 -1
- package/dist/plugins/store-backend.d.ts +12 -5
- package/dist/plugins/store-backend.js +13 -4
- package/dist/plugins/store-contracts.d.ts +20 -3
- package/dist/plugins/store-open-wait.js +3 -0
- package/dist/plugins/tidb-pool.js +15 -3
- package/dist/plugins/tidb-session-storage.js +3 -2
- package/dist/plugins/tidb-session-store.d.ts +2 -0
- package/dist/plugins/tidb-session-store.js +6 -2
- package/dist/request-key-closure.d.ts +32 -7
- package/dist/request-key-closure.js +25 -9
- package/dist/router/route-orchestration.d.ts +3 -11
- package/dist/router/route-orchestration.js +0 -4
- package/dist/rule-entry-list.js +0 -4
- package/dist/rules-consent.js +3 -0
- package/dist/run-acceptance.d.ts +51 -0
- package/dist/run-acceptance.js +27 -0
- package/dist/run-local.d.ts +5 -4
- package/dist/run-local.js +23 -17
- package/dist/runs.d.ts +7 -11
- package/dist/runs.js +6 -16
- package/dist/sandbox-egress-policy.d.ts +61 -0
- package/dist/sandbox-egress-policy.js +235 -0
- package/dist/security.d.ts +7 -0
- package/dist/security.js +5 -0
- package/dist/server-secret-env.d.ts +10 -4
- package/dist/server-secret-env.js +4 -1
- package/dist/session-error-wire.d.ts +2 -2
- package/dist/session-incarnation.d.ts +46 -2
- package/dist/session-incarnation.js +14 -3
- package/dist/session-sync-kernel.d.ts +10 -3
- package/dist/session-titler.d.ts +5 -3
- package/dist/spec-fields.d.ts +24 -8
- package/dist/spec-fields.js +21 -2
- package/dist/task-workflow.d.ts +6 -4
- package/dist/task-workflow.js +3 -2
- package/dist/tool-approval.d.ts +78 -6
- package/dist/tool-approval.js +7 -3
- package/dist/trace/core-keyset-guard.d.ts +6 -5
- package/dist/trace/engine-notice-wire.d.ts +26 -1
- package/dist/trace/engine-notice-wire.js +7 -2
- package/dist/trace/ledger-events.d.ts +9 -3
- package/dist/trace/ledger-sink.d.ts +7 -0
- package/dist/trace/ledger-sink.js +14 -4
- package/dist/trace/project.d.ts +57 -11
- package/dist/trace/project.js +54 -8
- package/dist/trace/self-minted-wire.d.ts +94 -2
- package/dist/trace/self-minted-wire.js +28 -0
- package/dist/trace/tool-output-media.d.ts +26 -0
- package/dist/trace/tool-output-media.js +57 -0
- package/dist/trace/wire-projection-faces.d.ts +7 -1
- package/dist/trace/wire-projection-faces.js +2 -1
- package/dist/web-admin-client/apply-effective.d.ts +13 -7
- package/dist/web-admin-client/apply-effective.js +11 -26
- package/dist/web-admin-client/apply-ledger.d.ts +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +39 -4
- package/dist/web-admin-client/candidate-gate.js +9 -2
- package/dist/web-admin-client/facade.d.ts +2 -2
- package/dist/web-admin-client/http-client.d.ts +3 -1
- package/dist/web-admin-client/http-client.js +4 -3
- package/dist/web-admin-client/read-warnings.d.ts +33 -5
- package/dist/web-admin-client/read-warnings.js +41 -6
- package/dist/web-admin-client/types.d.ts +11 -5
- package/dist/workspace-cache.d.ts +45 -0
- package/dist/workspace-cache.js +30 -0
- package/dist/workspace-space-commit.d.ts +116 -0
- package/dist/workspace-space-commit.js +117 -0
- package/dist/workspace-space.d.ts +75 -9
- package/dist/workspace-space.js +52 -1
- package/package.json +4 -4
- package/dist/boot/session-incarnation-seat.d.ts +0 -41
- package/dist/boot/session-incarnation-seat.js +0 -69
- package/dist/capabilities/code-review-council.d.ts +0 -49
- package/dist/capabilities/code-review-council.js +0 -111
- package/dist/capabilities/collab-wire.d.ts +0 -36
- package/dist/capabilities/collab-wire.js +0 -2
- package/dist/capabilities/collab-workflows.d.ts +0 -47
- package/dist/capabilities/collab-workflows.js +0 -90
- package/dist/capabilities/prompts/code-review.d.ts +0 -25
- package/dist/capabilities/prompts/code-review.js +0 -39
- package/dist/capabilities/prompts/team.d.ts +0 -10
- package/dist/capabilities/prompts/team.js +0 -6
- package/dist/capabilities/team.d.ts +0 -113
- package/dist/capabilities/team.js +0 -70
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import vm from "node:vm";
|
|
3
3
|
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
4
|
-
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, } from "@sema-agent/settings-schema/hooks";
|
|
4
|
+
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
|
|
5
5
|
import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
|
|
6
6
|
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
7
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
@@ -59,7 +59,8 @@ function toAssistantTurnError(errorKind) {
|
|
|
59
59
|
return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
|
|
60
60
|
}
|
|
61
61
|
export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
|
|
62
|
-
export const ADMISSION_MATCHER_EVAL_BUDGET_MS =
|
|
62
|
+
export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 1_000;
|
|
63
|
+
export const MATCHER_ECHO_CHARS = 200;
|
|
63
64
|
const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
|
|
64
65
|
function matcherReadsWireToolName(event) {
|
|
65
66
|
return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
|
|
@@ -81,10 +82,7 @@ function engineToolAliases() {
|
|
|
81
82
|
return table;
|
|
82
83
|
}
|
|
83
84
|
let matcherEvalBox;
|
|
84
|
-
function matcherSelectsWithin(matcher, name,
|
|
85
|
-
const remaining = budget.deadlineMs - budget.now();
|
|
86
|
-
if (remaining <= 0)
|
|
87
|
-
return "over_budget";
|
|
85
|
+
function matcherSelectsWithin(matcher, name, budgetMs) {
|
|
88
86
|
matcherEvalBox ??= {
|
|
89
87
|
script: new vm.Script("hookMatcherSelects(matcher, name)"),
|
|
90
88
|
context: vm.createContext({ hookMatcherSelects, matcher: "", name: "" }),
|
|
@@ -92,7 +90,7 @@ function matcherSelectsWithin(matcher, name, budget) {
|
|
|
92
90
|
matcherEvalBox.context.matcher = matcher;
|
|
93
91
|
matcherEvalBox.context.name = name;
|
|
94
92
|
try {
|
|
95
|
-
const verdict = matcherEvalBox.script.runInContext(matcherEvalBox.context, { timeout: Math.max(1, Math.ceil(
|
|
93
|
+
const verdict = matcherEvalBox.script.runInContext(matcherEvalBox.context, { timeout: Math.max(1, Math.ceil(budgetMs)) });
|
|
96
94
|
return verdict === true ? true : verdict === false ? false : "invalid";
|
|
97
95
|
}
|
|
98
96
|
catch (err) {
|
|
@@ -101,12 +99,21 @@ function matcherSelectsWithin(matcher, name, budget) {
|
|
|
101
99
|
throw err;
|
|
102
100
|
}
|
|
103
101
|
}
|
|
102
|
+
function matcherCompileSentence(matcher) {
|
|
103
|
+
try {
|
|
104
|
+
new RegExp(matcher);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return err instanceof Error ? err.message : String(err);
|
|
108
|
+
}
|
|
109
|
+
return "the pattern does not compile as a regular expression";
|
|
110
|
+
}
|
|
104
111
|
const MATCHER_EVALUATION = {
|
|
105
112
|
literal: { regex: "inline", literal: "inline" },
|
|
106
113
|
regex: { regex: "budgeted", literal: "not_applicable" },
|
|
107
114
|
invalid: { regex: "unreadable", literal: "not_applicable" },
|
|
108
115
|
};
|
|
109
|
-
function
|
|
116
|
+
function judgeMatcher(matcher, depth, budgetMs) {
|
|
110
117
|
if (matcher.length > MAX_MATCHER_EVAL_CHARS)
|
|
111
118
|
return { kind: "unjudged", reason: "oversize" };
|
|
112
119
|
const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
|
|
@@ -114,7 +121,7 @@ function deadAliasInMatcher(matcher, depth, budget) {
|
|
|
114
121
|
case "not_applicable":
|
|
115
122
|
return undefined;
|
|
116
123
|
case "unreadable":
|
|
117
|
-
return { kind: "
|
|
124
|
+
return { kind: "invalid_regex" };
|
|
118
125
|
case "inline":
|
|
119
126
|
case "budgeted":
|
|
120
127
|
break;
|
|
@@ -123,48 +130,74 @@ function deadAliasInMatcher(matcher, depth, budget) {
|
|
|
123
130
|
return unreachable;
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
|
-
const selects = (name) =>
|
|
127
|
-
|
|
128
|
-
return verdict === "invalid" ? "invalid_regex" : verdict;
|
|
129
|
-
};
|
|
133
|
+
const selects = (name) => (evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs));
|
|
134
|
+
const unanswered = (verdict) => (verdict === "over_budget" ? { kind: "over_budget" } : { kind: "invalid_regex" });
|
|
130
135
|
for (const [alias, canonical] of engineToolAliases()) {
|
|
131
136
|
const selectsAlias = selects(alias);
|
|
132
137
|
if (typeof selectsAlias === "string")
|
|
133
|
-
return
|
|
138
|
+
return unanswered(selectsAlias);
|
|
134
139
|
if (!selectsAlias)
|
|
135
140
|
continue;
|
|
136
141
|
const selectsCanonical = selects(canonical);
|
|
137
142
|
if (typeof selectsCanonical === "string")
|
|
138
|
-
return
|
|
143
|
+
return unanswered(selectsCanonical);
|
|
139
144
|
if (!selectsCanonical)
|
|
140
145
|
return { kind: "dead", alias, canonical };
|
|
141
146
|
}
|
|
142
147
|
return undefined;
|
|
143
148
|
}
|
|
144
|
-
export function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
export function buildToolMatcherRefusal(issue) {
|
|
150
|
+
switch (issue.reason) {
|
|
151
|
+
case "dead_alias":
|
|
152
|
+
return {
|
|
153
|
+
message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
|
|
154
|
+
field: "settings.hooks",
|
|
155
|
+
detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical },
|
|
156
|
+
};
|
|
157
|
+
case "over_budget":
|
|
158
|
+
return {
|
|
159
|
+
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating the pattern did not finish within ${issue.budgetMs} ms (catastrophic regex backtracking) — rewrite it as a tool-name list or a pattern without nested repetition`,
|
|
160
|
+
field: "settings.hooks",
|
|
161
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs },
|
|
162
|
+
};
|
|
163
|
+
case "invalid_regex":
|
|
164
|
+
return {
|
|
165
|
+
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: ${JSON.stringify(issue.matcher)} is not a valid regular expression (${issue.compileError}) — fix the pattern, or write the tool names as a list such as "Read|Edit"`,
|
|
166
|
+
field: "settings.hooks",
|
|
167
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex },
|
|
168
|
+
};
|
|
169
|
+
default: {
|
|
170
|
+
const unreachable = issue;
|
|
171
|
+
return unreachable;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
150
174
|
}
|
|
151
|
-
export function
|
|
152
|
-
const
|
|
153
|
-
const deadline = { deadlineMs: now() + (budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS), now };
|
|
175
|
+
export function toolMatcherIssue(config, depth, budget = {}) {
|
|
176
|
+
const budgetMs = budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS;
|
|
154
177
|
for (const [event, groups] of Object.entries(config)) {
|
|
155
178
|
if (!matcherReadsWireToolName(event))
|
|
156
179
|
continue;
|
|
157
|
-
for (const group of groups ?? []) {
|
|
180
|
+
for (const [groupIndex, group] of (groups ?? []).entries()) {
|
|
158
181
|
if (group.matcher === undefined)
|
|
159
182
|
continue;
|
|
160
|
-
const verdict =
|
|
183
|
+
const verdict = judgeMatcher(group.matcher, depth, budgetMs);
|
|
161
184
|
if (verdict === undefined)
|
|
162
185
|
continue;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
switch (verdict.kind) {
|
|
187
|
+
case "dead":
|
|
188
|
+
return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
|
|
189
|
+
case "over_budget":
|
|
190
|
+
return { reason: "over_budget", event, groupIndex, budgetMs };
|
|
191
|
+
case "invalid_regex":
|
|
192
|
+
return { reason: "invalid_regex", event, groupIndex, matcher: clip(group.matcher, MATCHER_ECHO_CHARS), compileError: clip(matcherCompileSentence(group.matcher), MATCHER_ECHO_CHARS) };
|
|
193
|
+
case "unjudged":
|
|
194
|
+
recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
|
|
195
|
+
break;
|
|
196
|
+
default: {
|
|
197
|
+
const unreachable = verdict;
|
|
198
|
+
return unreachable;
|
|
199
|
+
}
|
|
166
200
|
}
|
|
167
|
-
return { event, alias: verdict.alias, canonical: verdict.canonical };
|
|
168
201
|
}
|
|
169
202
|
}
|
|
170
203
|
return undefined;
|
|
@@ -558,19 +591,19 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
558
591
|
clearTimeout(hardTopTimer);
|
|
559
592
|
}
|
|
560
593
|
}
|
|
561
|
-
|
|
594
|
+
const MAX_BLANKED_REFERENCES_LOGGED = 16;
|
|
595
|
+
async function runHttpHook(entry, payload, ctx, event) {
|
|
562
596
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const ev = envSource[name];
|
|
571
|
-
return typeof ev === "string" ? ev : whole;
|
|
597
|
+
const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx));
|
|
598
|
+
if (interpolated.blanked.length > 0) {
|
|
599
|
+
ctx.logger.warn("hook_http_header_env_blanked", {
|
|
600
|
+
event,
|
|
601
|
+
url: buildDisplaySafeUrl(entry.url),
|
|
602
|
+
blankedCount: interpolated.blanked.length,
|
|
603
|
+
blanked: interpolated.blanked.slice(0, MAX_BLANKED_REFERENCES_LOGGED).map(({ header, name, reason }) => ({ header, name, reason })),
|
|
572
604
|
});
|
|
573
605
|
}
|
|
606
|
+
const headers = { "content-type": "application/json", ...interpolated.headers };
|
|
574
607
|
const ac = new AbortController();
|
|
575
608
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
576
609
|
try {
|
|
@@ -732,7 +765,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
|
|
|
732
765
|
onceFired.add(onceKey);
|
|
733
766
|
if (entry.type === "prompt" || entry.type === "agent")
|
|
734
767
|
llmEntries++;
|
|
735
|
-
const run = entry.type === "http" ? await runHttpHook(entry, payload, ctx)
|
|
768
|
+
const run = entry.type === "http" ? await runHttpHook(entry, payload, ctx, event)
|
|
736
769
|
: entry.type === "prompt" || entry.type === "agent" ? await runLlmHook(entry, payload, ctx, entry.type === "prompt" ? llmExtra : { system: ccAgentHookSystemFor(event), event })
|
|
737
770
|
: await runCommandHook(entry, payload, ctx);
|
|
738
771
|
if (run.spawnError) {
|
package/dist/http/admission.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isPermissionMode, isThinkingLevel, expandTiers } from "@sema-agent/core";
|
|
2
2
|
import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
|
|
3
3
|
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
4
|
-
import { HttpError } from "../security.js";
|
|
4
|
+
import { HttpError, sessionIdShapeIssue } from "../security.js";
|
|
5
5
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, stampEntryPermissionMode, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
6
6
|
import { autonomyModeFloor } from "../runtime-governance.js";
|
|
7
7
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
8
8
|
import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
9
9
|
import * as facts from "../capability-facts.js";
|
|
10
|
-
import {
|
|
10
|
+
import { buildToolMatcherRefusal, parseHooksConfig, toolMatcherIssue } from "../hooks/hook-runner.js";
|
|
11
11
|
import { judgeRequestWebSearch } from "../plugins/web-search.js";
|
|
12
12
|
import { normalizeApproachNotice, skillToolListOk, skillToolListRefusal, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
13
13
|
import { MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
14
14
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
15
15
|
import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
|
|
16
16
|
import { resolveDevicePlacementIntent } from "./device-placement.js";
|
|
17
|
+
import { createRunAcceptance } from "../run-acceptance.js";
|
|
17
18
|
import { sendError, httpErrorCode } from "./send.js";
|
|
18
19
|
import { ABSOLUTE_HOST_PATH_MAX, isAbsoluteHostPath, WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
19
20
|
const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
|
|
@@ -62,6 +63,7 @@ function refuseUnavailableModel(res, config, field, ref) {
|
|
|
62
63
|
sendError(res, 400, "request.model_unavailable", message);
|
|
63
64
|
return true;
|
|
64
65
|
}
|
|
66
|
+
const modelIdOf = (model) => (typeof model === "string" ? model : model?.id);
|
|
65
67
|
export function createAdmission(ctx) {
|
|
66
68
|
const { deps, readJson, runnerFor } = ctx;
|
|
67
69
|
async function prepareSpec(req, res, bodyIn, onTypedFailure, legFacts) {
|
|
@@ -85,9 +87,12 @@ export function createAdmission(ctx) {
|
|
|
85
87
|
sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
|
|
86
88
|
return null;
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
{
|
|
91
|
+
const sessionIdIssue = sessionIdShapeIssue(body.sessionId);
|
|
92
|
+
if (sessionIdIssue) {
|
|
93
|
+
sendError(res, 400, "request.id_invalid", sessionIdIssue);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
91
96
|
}
|
|
92
97
|
if (typeof body.systemPrompt === "string" && body.systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
93
98
|
sendError(res, 400, "request.field_invalid", `systemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
|
|
@@ -212,9 +217,9 @@ export function createAdmission(ctx) {
|
|
|
212
217
|
sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
|
|
213
218
|
return null;
|
|
214
219
|
}
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
const refusal =
|
|
220
|
+
const toolMatcher = toolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
|
|
221
|
+
if (toolMatcher !== undefined) {
|
|
222
|
+
const refusal = buildToolMatcherRefusal(toolMatcher);
|
|
218
223
|
sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
|
|
219
224
|
return null;
|
|
220
225
|
}
|
|
@@ -502,7 +507,7 @@ export function createAdmission(ctx) {
|
|
|
502
507
|
return null;
|
|
503
508
|
}
|
|
504
509
|
}
|
|
505
|
-
for (const key of ["excludeTools", "deferTools"]) {
|
|
510
|
+
for (const key of ["allowTools", "excludeTools", "deferTools"]) {
|
|
506
511
|
const v = body[key];
|
|
507
512
|
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
508
513
|
sendError(res, 400, "request.field_invalid", `${key} must be an array of non-empty tool-name strings`);
|
|
@@ -637,7 +642,9 @@ export function createAdmission(ctx) {
|
|
|
637
642
|
store: deps.deviceStore,
|
|
638
643
|
enrollment: deps.deviceEnrollment,
|
|
639
644
|
});
|
|
640
|
-
|
|
645
|
+
const model = modelIdOf(spec.model);
|
|
646
|
+
const acceptance = createRunAcceptance({ sessionTitler: deps.sessionTitler }, { ...(spec.sessionId ? { sessionId: spec.sessionId } : {}), objective: spec.objective, ...(model !== undefined ? { model } : {}) });
|
|
647
|
+
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, acceptance, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(engineNotices.length > 0 ? { engineNotices } : {}) };
|
|
641
648
|
}
|
|
642
649
|
catch (err) {
|
|
643
650
|
if (err instanceof HttpError) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { type RunInternals } from "@sema-agent/core";
|
|
17
17
|
import type { RunStore } from "../plugins/store-backend.js";
|
|
18
|
-
import type { RunRecord } from "../plugins/store-contracts.js";
|
|
18
|
+
import type { RunIdemBinding, RunRecord } from "../plugins/store-contracts.js";
|
|
19
19
|
import type { PreparedTaskSubmission, RouteCtxBase } from "./route-ctx.js";
|
|
20
20
|
/**
|
|
21
21
|
* 🔴 **TRUSTED run internals** —— 由**部署方**(不是模型、不是请求体、不是工具参数)铸给这一跑的
|
|
@@ -35,8 +35,9 @@ export interface DurableRunLegArgs {
|
|
|
35
35
|
prepared: PreparedTaskSubmission;
|
|
36
36
|
/** 调用方自带的 taskId(`POST /v1/runs` 的 `body.taskId` 幂等键);缺席 ⇒ 本腿铸 uuidv7。 */
|
|
37
37
|
clientTaskId?: string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/** 这次提交的账本绑定(键 `idemKeyOf(raw, caller, "run")` + 请求体指纹)—— 落 `task_run.idem_key` / `idem_body_fingerprint`;
|
|
39
|
+
* 缺席 = 调用方没带 `Idempotency-Key`。 */
|
|
40
|
+
idem?: RunIdemBinding;
|
|
40
41
|
/** 🔒 这条 run 的 **fleet 行 scope** —— 必须与 `GET /v1/fleet/stream` 过滤用的是同一个身份,且绝不是
|
|
41
42
|
* 自报的 `x-agent-principal` 头(自报头会把行打到受害者的 scope 上,或打到 `default` 而真属主的流把它
|
|
42
43
|
* 过滤掉)。提交门传 `gatedPrincipal(req) ?? "default"`;`/decide` 的第三条车道传 resume 家族的
|
|
@@ -71,7 +72,7 @@ export type DurableRunLegOutcome = {
|
|
|
71
72
|
/**
|
|
72
73
|
* 建行 + 驱动。返回 `202` = 新行已建并在跑(体带 `taskId`/`sessionId`);`409` = 会话被别的 run 占着,
|
|
73
74
|
* **没有**建行(体 = `buildActiveRunConflict` 的既有形,含 `errorCode: "conflict.session_active_run"`);
|
|
74
|
-
* `existing` = 这次提交的钥匙(`
|
|
75
|
+
* `existing` = 这次提交的钥匙(`idem.key` / `clientTaskId`)已经指向一条行,没建新行。
|
|
75
76
|
* 协议渲染归调用方(HTTP `sendJson` / A2A 协议对象 / `/decide` 的受理体)。
|
|
76
77
|
*/
|
|
77
78
|
export declare function mintAndDriveDurableRun(ctx: RouteCtxBase, runStore: RunStore, args: DurableRunLegArgs): Promise<DurableRunLegOutcome>;
|
|
@@ -15,13 +15,11 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
|
15
15
|
const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
|
|
16
16
|
const sessionId = args.prepared.spec.sessionId ?? uuidv7();
|
|
17
17
|
const taskId = args.clientTaskId ?? uuidv7();
|
|
18
|
-
const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", { ...runMeta(args.prepared, args.source),
|
|
18
|
+
const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", { ...runMeta(args.prepared, args.source), idem: args.idem ?? null });
|
|
19
19
|
if (created.ok)
|
|
20
20
|
clearTurnActivity(taskId);
|
|
21
21
|
if (created.ok)
|
|
22
22
|
recordResourceWindow(taskId, args.prepared.spec.resourceSuspend);
|
|
23
|
-
if (created.ok)
|
|
24
|
-
deps.sessionTitler?.maybeTitle(sessionId, args.prepared.spec.objective, typeof args.prepared.spec.model === "string" ? args.prepared.spec.model : args.prepared.spec.model?.id);
|
|
25
23
|
if (created.existing)
|
|
26
24
|
return { existing: created.existing };
|
|
27
25
|
if (!created.ok) {
|
|
@@ -65,7 +63,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
|
65
63
|
windowMs: deps.config.streamApproval?.windowMs,
|
|
66
64
|
windowMarginMs: deps.config.streamAskWindowMarginMs,
|
|
67
65
|
}
|
|
68
|
-
: undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals, deps.userInputLedger);
|
|
66
|
+
: undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals, deps.userInputLedger, args.prepared.acceptance);
|
|
69
67
|
return { status: 202, body: { taskId, sessionId, status: "running" } };
|
|
70
68
|
}
|
|
71
69
|
//# sourceMappingURL=durable-run-leg.js.map
|
|
@@ -2,6 +2,7 @@ import type { IncomingMessage } from "node:http";
|
|
|
2
2
|
import type { ServiceConfig } from "../config-types.js";
|
|
3
3
|
import type { WiringManifestReceipt } from "./wire-types.js";
|
|
4
4
|
import type { SubmitReplayFace } from "./submit-replay.js";
|
|
5
|
+
import type { RunIdemBinding } from "../plugins/store-contracts.js";
|
|
5
6
|
/**
|
|
6
7
|
* 同进程的**在飞合流** —— 并发同键的提交共享同一个 promise(避免同一进程里双起)。它**不是**重放的真源。
|
|
7
8
|
*
|
|
@@ -78,14 +79,35 @@ export declare function idemIdentityOf(caller: IdemCaller): string;
|
|
|
78
79
|
*/
|
|
79
80
|
export declare function idemKeyOf(raw: string, caller: IdemCaller, family: IdemFamily): string;
|
|
80
81
|
/**
|
|
81
|
-
*
|
|
82
|
+
* 一次提交的**请求体指纹** —— HTTP 三条提交腿(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)同一只规范化函数:
|
|
83
|
+
* `sha256(canonicalJson(体 − taskId))` 的 hex(定宽 64)。
|
|
82
84
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* 幂等 = 同一调用方、同一键、**同一请求**:账本行记下绑定 `Idempotency-Key` 的那次请求的指纹(`task_run.idem_body_fingerprint`),
|
|
86
|
+
* 同键再投而指纹不同 ⇒ 唯一回执函数答 409(`http/submit-replay.ts`)。规范化只有两步:
|
|
87
|
+
* · JSON 按键递归排序(唯一序列化器 `canonicalJson`;值为 `undefined` 的键与缺席同义)—— 键序不同的同一份体是同一请求;
|
|
88
|
+
* · 去掉 `taskId` —— 它是同一条律的**另一把钥匙**(调用方自铸的主键),由回执函数的「钥匙指向别的行 ⇒ `conflict.run_exists`」判,
|
|
89
|
+
* 不是请求内容(换腿重投:流式腿不带 → durable 腿带回 `x-task-id`,仍是同一请求)。
|
|
90
|
+
* 头一概不进:身份已在键里(`idemKeyOf` 的身份位),其余头(`Idempotency-Key` 本身、`x-detach-on-disconnect` 一类连接语义)不是提交内容。
|
|
91
|
+
* 入参是**线上收到的那份体**(`readJson` 的产物),不是 `prepareSpec` 之后服务端盖过章的体 —— 指纹只由调用方看得见的字节构成,
|
|
92
|
+
* 否则两台配置不同的副本对同一次重试算出两只指纹。
|
|
87
93
|
*/
|
|
88
|
-
export declare function
|
|
94
|
+
export declare function submissionFingerprintOf(body: unknown): string;
|
|
95
|
+
/**
|
|
96
|
+
* HTTP 提交腿(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)的**账本绑定**唯一铸法:键 = `idemKeyOf(raw, caller, "run")`,
|
|
97
|
+
* 指纹 = {@link submissionFingerprintOf}(线上收到的那份体)。三条腿在读到 `Idempotency-Key` 的同一处调它 —— 键与指纹同铸,不各拼一份。
|
|
98
|
+
* (A2A 的绑定不经这里:它的原始键就是消息内容指纹,绑定 = `{ key: idemKeyOf(指纹, …), bodyFingerprint: 指纹 }`,见 `routes/a2a-serve.ts`。)
|
|
99
|
+
*/
|
|
100
|
+
export declare function submissionIdemOf(raw: string, caller: IdemCaller, body: unknown): RunIdemBinding;
|
|
101
|
+
/**
|
|
102
|
+
* 在飞合流的键 = 这次提交的**账本绑定**(幂等键 × 请求体指纹)× 这条腿的**回执形**(`durable` / `sync`)× 这次请求自带的 taskId。
|
|
103
|
+
*
|
|
104
|
+
* 合流交出的是**首个请求那条腿的结局**,所以只有「同一请求、同一条 run、同一种回执形、同一把 taskId」的请求才许合流:同键异体的
|
|
105
|
+
* 请求合进来就是静默拿到别的请求的结局(正是 409 `conflict.idempotency_payload_mismatch` 要拦的那件事);durable 腿合到同步腿的在飞
|
|
106
|
+
* 结局上会拿到 200 引擎结果而不是 202 收据;带另一个 taskId 的请求合进来就绕过了「key 指向别的行」那条 409。不许合流的请求各自走到
|
|
107
|
+
* 读行 / 建行,由店的原子赢或观察 + 唯一回执函数作答 —— 同体的仍是一条 run、各拿自己腿的回执形,异体的拿 409。同步腿与流式腿同形
|
|
108
|
+
* (流式腿把同一份体放进 `done.result`),合流照旧。
|
|
109
|
+
*/
|
|
110
|
+
export declare function inflightKeyOf(idem: RunIdemBinding, face: SubmitReplayFace, clientTaskId?: string): string;
|
|
89
111
|
/**
|
|
90
112
|
* 提交腿的**运行结局**(在飞合流共享的那一只)—— `status` + `body` 是它一直以来的两位;S-528 起多一位
|
|
91
113
|
* `wiringManifest`。
|
package/dist/http/idempotency.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalJson } from "../canonical-json.js";
|
|
2
3
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
3
4
|
export class IdempotencyCache {
|
|
4
5
|
ttlMs;
|
|
@@ -77,7 +78,14 @@ export function idemIdentityOf(caller) {
|
|
|
77
78
|
export function idemKeyOf(raw, caller, family) {
|
|
78
79
|
return createHash("sha256").update(`${idemIdentityOf(caller)}\x1f${idemFamilyTag(family)}\x1f${raw}`).digest("hex");
|
|
79
80
|
}
|
|
80
|
-
export function
|
|
81
|
-
|
|
81
|
+
export function submissionFingerprintOf(body) {
|
|
82
|
+
const content = body !== null && typeof body === "object" && !Array.isArray(body) ? { ...body, taskId: undefined } : body;
|
|
83
|
+
return createHash("sha256").update(canonicalJson(content)).digest("hex");
|
|
84
|
+
}
|
|
85
|
+
export function submissionIdemOf(raw, caller, body) {
|
|
86
|
+
return { key: idemKeyOf(raw, caller, "run"), bodyFingerprint: submissionFingerprintOf(body) };
|
|
87
|
+
}
|
|
88
|
+
export function inflightKeyOf(idem, face, clientTaskId) {
|
|
89
|
+
return `${idem.key}\x1f${idem.bodyFingerprint}\x1f${face}\x1f${clientTaskId ?? ""}`;
|
|
82
90
|
}
|
|
83
91
|
//# sourceMappingURL=idempotency.js.map
|
|
@@ -92,6 +92,20 @@ export declare function sendUnauthorized(res: ServerResponse, face: Unauthorized
|
|
|
92
92
|
* 那样「谁多给了 operator 一条路」在代码上看得见,而不是被偷偷塞进共享谓词里。
|
|
93
93
|
*/
|
|
94
94
|
export declare function runOwnerReadable(principal: string | undefined, owner: string | null, config: ServiceConfig): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* 🔴 **「这台部署有没有身份边界」的唯一判据**(S-1452)。dev-open = 没配 service token(单一或 per-system)、没配只读 trace token、
|
|
97
|
+
* 也没开 `REQUIRE_PRINCIPAL`:请求里没有任何东西替主体头作保,头是谁都能自报的**标签**(过滤 / 展示用),不是身份。
|
|
98
|
+
* 三处读它、不各写一份:会话列表的 fleet-wide 第一臂(`isFleetWide`)、会话写面的 fleet-wide 第一臂(`sessionOwnerScopeForWrite`)、
|
|
99
|
+
* 会话面的属主谓词({@link sessionOwnerReadable})—— 于是「调用方是谁」在列表与单会话面上只有一只读法。
|
|
100
|
+
*/
|
|
101
|
+
export declare function devOpenDeployment(config: ServiceConfig): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* **「这个调用方能不能动这只会话」的唯一谓词**(会话面:视图 / head / 事件流 / 记忆状态与自助抹除 / 改名置顶 / 后台任务 / MCP 重连 /
|
|
104
|
+
* 面板的上一腿 MCP 投影)。= {@link devOpenDeployment} ∨ {@link runOwnerReadable}:没有身份边界的部署上不钉任何人(与会话列表同读,
|
|
105
|
+
* 列表把这只会话列给谁,谁就读得到它);有身份边界的部署上就是 run 面那一条谓词,一字不变。
|
|
106
|
+
* run 面(poll / events / cancel …)不读本谓词 —— 那是 run 资源的门,另一只。
|
|
107
|
+
*/
|
|
108
|
+
export declare function sessionOwnerReadable(principal: string | undefined, owner: string | null, config: ServiceConfig): boolean;
|
|
95
109
|
/** The EXPLICIT operator gate (IMAGE-API-DESIGN.md §P2.4a MUST-FIX) — NOT the bare `isOperator` (whose empty-list
|
|
96
110
|
* "true-for-all" is an unrestricted-write exec door the instant OPERATOR_PRINCIPALS is empty). Requires a non-empty
|
|
97
111
|
* operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
|
|
@@ -93,6 +93,13 @@ export function runOwnerReadable(principal, owner, config) {
|
|
|
93
93
|
return true;
|
|
94
94
|
return !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
|
|
95
95
|
}
|
|
96
|
+
export function devOpenDeployment(config) {
|
|
97
|
+
const anyServiceToken = Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
|
|
98
|
+
return !anyServiceToken && !config.traceToken && !config.requirePrincipal;
|
|
99
|
+
}
|
|
100
|
+
export function sessionOwnerReadable(principal, owner, config) {
|
|
101
|
+
return devOpenDeployment(config) || runOwnerReadable(principal, owner, config);
|
|
102
|
+
}
|
|
96
103
|
export function explicitOperatorOk(principal, operatorPrincipals) {
|
|
97
104
|
return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
|
|
98
105
|
}
|
|
@@ -84,7 +84,8 @@ export declare function createRequestGuards(ctx: {
|
|
|
84
84
|
fleetWide: boolean;
|
|
85
85
|
gateOwner: string | null;
|
|
86
86
|
};
|
|
87
|
-
runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null
|
|
87
|
+
runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
|
|
88
|
+
sessionOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
|
|
88
89
|
runSessionAcceptOk: (req: IncomingMessage, res: ServerResponse, run: {
|
|
89
90
|
sessionId: string | null;
|
|
90
91
|
}, face: string, notFoundError?: string) => boolean;
|
|
@@ -2,7 +2,7 @@ import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
|
2
2
|
import { HttpError, principalFrom } from "../security.js";
|
|
3
3
|
import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
|
|
4
4
|
import { sendError } from "./send.js";
|
|
5
|
-
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
|
|
5
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sessionOwnerReadable, devOpenDeployment, sendPrincipalRefusal } from "./principal-gate.js";
|
|
6
6
|
import { quotaWindowResetsAt } from "../observability/cost-quota.js";
|
|
7
7
|
const MAX_BODY = 8 * 1024 * 1024;
|
|
8
8
|
export function leaseRefusalOf(adm) {
|
|
@@ -150,9 +150,8 @@ export function createRequestGuards(ctx) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
function isFleetWide(req) {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
|
|
153
|
+
const tt = deps.config.traceToken;
|
|
154
|
+
return devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined || (!!tt && authorized(req, tt));
|
|
156
155
|
}
|
|
157
156
|
function sessionOwnerScope(req) {
|
|
158
157
|
const fleetWide = isFleetWide(req);
|
|
@@ -160,27 +159,28 @@ export function createRequestGuards(ctx) {
|
|
|
160
159
|
return { fleetWide, gateOwner };
|
|
161
160
|
}
|
|
162
161
|
function sessionOwnerScopeForWrite(req) {
|
|
163
|
-
const
|
|
164
|
-
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
165
|
-
const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
|
|
162
|
+
const fleetWide = devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined;
|
|
166
163
|
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
167
164
|
return { fleetWide, gateOwner };
|
|
168
165
|
}
|
|
169
|
-
function
|
|
166
|
+
function ownerGate(req, res, readable, notFound) {
|
|
170
167
|
const principal = gatedPrincipal(req, deps.config);
|
|
171
168
|
if (deps.config.requirePrincipal && !principal) {
|
|
172
169
|
sendPrincipalRefusal(res, req, deps.config);
|
|
173
170
|
return false;
|
|
174
171
|
}
|
|
175
|
-
if (!
|
|
176
|
-
|
|
177
|
-
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
178
|
-
else
|
|
179
|
-
sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
|
|
172
|
+
if (!readable(principal)) {
|
|
173
|
+
notFound();
|
|
180
174
|
return false;
|
|
181
175
|
}
|
|
182
176
|
return true;
|
|
183
177
|
}
|
|
178
|
+
function runOwnerOk(req, res, owner) {
|
|
179
|
+
return ownerGate(req, res, (principal) => runOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE));
|
|
180
|
+
}
|
|
181
|
+
function sessionOwnerOk(req, res, owner) {
|
|
182
|
+
return ownerGate(req, res, (principal) => sessionOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE));
|
|
183
|
+
}
|
|
184
184
|
const sessionAssertAbsentWarned = new Set();
|
|
185
185
|
function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
|
|
186
186
|
if (!run.sessionId)
|
|
@@ -229,6 +229,6 @@ export function createRequestGuards(ctx) {
|
|
|
229
229
|
}
|
|
230
230
|
return Buffer.concat(chunks);
|
|
231
231
|
}
|
|
232
|
-
return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody };
|
|
232
|
+
return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody };
|
|
233
233
|
}
|
|
234
234
|
//# sourceMappingURL=request-guards.js.map
|
|
@@ -15,7 +15,7 @@ import type { IncomingMessage } from "node:http";
|
|
|
15
15
|
import { CheckpointError, type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
|
|
16
16
|
import type { DecideBinding } from "./wire-types.js";
|
|
17
17
|
import type { CheckpointStoreFull } from "../plugins/store-backend.js";
|
|
18
|
-
import {
|
|
18
|
+
import type { HostDecision } from "../host-decision.js";
|
|
19
19
|
import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
|
|
20
20
|
import type { RouteCtxBase, DriveResumeArgs, HostLegOutcome, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
|
|
21
21
|
import { type TrustedRunInternals } from "./durable-run-leg.js";
|
|
@@ -74,7 +74,7 @@ export declare function createResumeLegs(ctx: {
|
|
|
74
74
|
readonly ok: true;
|
|
75
75
|
readonly prepared: PreparedPlanReview;
|
|
76
76
|
}>;
|
|
77
|
-
drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
|
|
77
|
+
drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, hostDecision: HostDecision, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
|
|
78
78
|
status: number;
|
|
79
79
|
body: object;
|
|
80
80
|
}>;
|