@sema-agent/server 7.56.0 → 7.58.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/README.md +3 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +101 -9
- package/dist/approval-ask-audit-store.d.ts +100 -1
- package/dist/approval-ask-audit-store.js +103 -2
- package/dist/approval-card.d.ts +148 -13
- package/dist/approval-card.js +82 -13
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +18 -0
- package/dist/auto-mode-face.d.ts +111 -0
- package/dist/auto-mode-face.js +99 -0
- package/dist/bench/s1/arms.js +5 -5
- package/dist/bench/s1/live-deps.js +11 -11
- package/dist/bench/s1/run-firm.js +3 -0
- package/dist/bench/s1/runner-ctx.d.ts +4 -0
- package/dist/bench/s1/runner-ctx.js +7 -0
- package/dist/boot/config-center.js +23 -1
- package/dist/boot/coordinators.js +13 -1
- package/dist/boot/parked-revive-gate.d.ts +7 -2
- package/dist/boot/parked-revive-gate.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/runner-deps.d.ts +12 -0
- package/dist/boot/runner-deps.js +28 -3
- package/dist/boot/runtime-caps.d.ts +19 -9
- package/dist/boot/runtime-caps.js +49 -21
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/boot/stores.js +12 -5
- package/dist/config-catalog.js +14 -5
- package/dist/config-center/http-client.js +7 -11
- package/dist/config-center/read-warnings.d.ts +20 -0
- package/dist/config-center/read-warnings.js +36 -0
- package/dist/config-provider.d.ts +3 -0
- package/dist/config-provider.js +4 -8
- package/dist/config-types.d.ts +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/hooks/hook-runner.js +8 -8
- package/dist/http/route-ctx.d.ts +75 -0
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +19 -6
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/http/routes/rules.js +3 -2
- package/dist/http/routes/runs.d.ts +0 -14
- package/dist/http/routes/runs.js +20 -8
- package/dist/http/routes/tasks.js +34 -8
- package/dist/http/routes/workflows.js +19 -4
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +215 -43
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +45 -12
- package/dist/leader/fanout.js +6 -5
- package/dist/leader/leader.js +17 -14
- package/dist/leader/merge.js +17 -12
- package/dist/leader/planner.js +3 -2
- package/dist/leader/repair-oracle.js +6 -4
- package/dist/leader/repair-wire.js +5 -3
- package/dist/leader/wire.d.ts +30 -1
- package/dist/leader/wire.js +36 -19
- package/dist/main.js +43 -7
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +10 -0
- package/dist/observability/fail-open.d.ts +40 -0
- package/dist/observability/fail-open.js +19 -0
- package/dist/observability/metrics.js +2 -2
- package/dist/observability/run-terminal-log.d.ts +120 -0
- package/dist/observability/run-terminal-log.js +360 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
- package/dist/orchestration/workflow-completion-inbox.js +8 -0
- package/dist/permission-rule-vocab.d.ts +40 -0
- package/dist/permission-rule-vocab.js +17 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +19 -3
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +19 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +3 -0
- package/dist/plugins/mailbox-store-sql.d.ts +28 -9
- package/dist/plugins/mailbox-store-sql.js +72 -10
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +19 -0
- package/dist/plugins/permission-rule-store-file.js +8 -3
- package/dist/plugins/permission-rule-store-sql.js +16 -10
- package/dist/plugins/remote-scratchpad.js +3 -2
- package/dist/plugins/run-store-sql.d.ts +3 -42
- package/dist/plugins/run-store-sql.js +30 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +66 -1
- package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
- package/dist/plugins/workflow-run-store-sql.js +10 -2
- package/dist/rules-consent.js +20 -13
- package/dist/run-cancel-context.d.ts +13 -0
- package/dist/run-cancel-context.js +15 -0
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +4 -1
- package/dist/runs.js +41 -10
- package/dist/runtime-caps-resolver.d.ts +133 -17
- package/dist/runtime-caps-resolver.js +41 -3
- package/dist/task-settings.d.ts +57 -3
- package/dist/task-settings.js +78 -5
- package/dist/task-workflow.d.ts +32 -2
- package/dist/task-workflow.js +8 -3
- package/dist/tool-approval.d.ts +26 -3
- package/dist/tool-approval.js +96 -13
- package/dist/trace/core-keyset-guard.d.ts +7 -8
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +58 -3
- package/dist/trace/project.js +33 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +660 -21
- package/dist/trace/sema-provenance.d.ts +35 -0
- package/dist/trace/sema-provenance.js +14 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +3 -3
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope,
|
|
5
|
+
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules } from "@sema-agent/core";
|
|
6
6
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
|
|
7
7
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
8
8
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
@@ -521,7 +521,8 @@ function noticeAutoCompactAboveWindow(slot, envName, m) {
|
|
|
521
521
|
model: m.id,
|
|
522
522
|
value: String(act),
|
|
523
523
|
contextWindow: String(window),
|
|
524
|
-
|
|
524
|
+
clampedTo: String(window),
|
|
525
|
+
note: `${envName}=${act} is ABOVE the ${slot} model "${m.id}" physical context window (${window}) — core uses autoCompactTokens as the TRIGGER-side window and CLAMPS it to the physical window (resolveTriggerWindow), so this knob is a NO-OP here: the trigger geometry falls back to ${window} and core mints one config.autocompact_window_clamped notice per prepare. server neither clamps nor rejects the value (clamping is core's, at the read site); lower it below the window to make the knob do something — CC's own shape is window-33000.`,
|
|
525
526
|
},
|
|
526
527
|
});
|
|
527
528
|
}
|
|
@@ -637,8 +638,8 @@ function parseModelDomain() {
|
|
|
637
638
|
return "gateway";
|
|
638
639
|
})();
|
|
639
640
|
const extraBody = buildExtraBody();
|
|
640
|
-
const thinkingDefault = (
|
|
641
|
-
const effortLevels = (
|
|
641
|
+
const thinkingDefault = parseDefaultThinking(process.env.MODEL_DEFAULT_THINKING);
|
|
642
|
+
const effortLevels = parseReasoningEffortLevels(csv("MODEL_REASONING_EFFORT_LEVELS"));
|
|
642
643
|
const model = {
|
|
643
644
|
id: modelId,
|
|
644
645
|
name: modelId,
|
|
@@ -777,6 +778,32 @@ function parseModelDomain() {
|
|
|
777
778
|
: undefined,
|
|
778
779
|
};
|
|
779
780
|
}
|
|
781
|
+
const THINKING_LEVEL_TABLE = { minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
|
|
782
|
+
export const THINKING_LEVELS = Object.keys(THINKING_LEVEL_TABLE);
|
|
783
|
+
const isThinkingTier = (w) => THINKING_LEVELS.includes(w);
|
|
784
|
+
function parseDefaultThinking(raw) {
|
|
785
|
+
const v = raw?.trim();
|
|
786
|
+
if (v === undefined || v === "")
|
|
787
|
+
return undefined;
|
|
788
|
+
if (v === "off")
|
|
789
|
+
return undefined;
|
|
790
|
+
if (!isThinkingTier(v)) {
|
|
791
|
+
throw new Error(`env MODEL_DEFAULT_THINKING="${v}" is not a thinking tier — valid: ${THINKING_LEVELS.join(" | ")} | off ` +
|
|
792
|
+
`("off" = no model-level default). A typo used to be dropped SILENTLY, leaving the deployment on no default at all (#210 A 档).`);
|
|
793
|
+
}
|
|
794
|
+
return v;
|
|
795
|
+
}
|
|
796
|
+
function parseReasoningEffortLevels(words) {
|
|
797
|
+
if (words.length === 0)
|
|
798
|
+
return [];
|
|
799
|
+
const bad = words.filter((w) => !isThinkingTier(w));
|
|
800
|
+
if (bad.length > 0) {
|
|
801
|
+
throw new Error(`env MODEL_REASONING_EFFORT_LEVELS has unknown tier(s): ${bad.join(", ")} (valid: ${THINKING_LEVELS.join(" | ")})` +
|
|
802
|
+
`${bad.includes("off") ? ' — "off" belongs to MODEL_DEFAULT_THINKING (no model-level default), it is not an effort tier an endpoint can accept' : ""}. ` +
|
|
803
|
+
`Bad words used to be dropped SILENTLY, narrowing the declared ladder without a word (#210 A 档).`);
|
|
804
|
+
}
|
|
805
|
+
return words.filter(isThinkingTier);
|
|
806
|
+
}
|
|
780
807
|
const DEGRADE_ON_WORDS = new Set(["breaker_open", "rate_limit", "budget", "server_error", "last_resort"]);
|
|
781
808
|
function parseDegradeOn(words) {
|
|
782
809
|
if (words.length === 0)
|
|
@@ -853,6 +880,7 @@ export function formatUnmatchableToolNames(source, bad) {
|
|
|
853
880
|
return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
|
|
854
881
|
bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
855
882
|
}
|
|
883
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
856
884
|
function streamApprovalConfig(gates) {
|
|
857
885
|
const cfg = {
|
|
858
886
|
enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
|
|
@@ -866,6 +894,12 @@ function streamApprovalConfig(gates) {
|
|
|
866
894
|
orphanTtlMs: numEnvBounded("STREAM_APPROVAL_ORPHAN_TTL_MS", String(7 * 24 * 60 * 60 * 1000), 60_000, 90 * 24 * 60 * 60 * 1000),
|
|
867
895
|
unreachedTtlMs: numEnvBounded("UNREACHED_ASK_TTL_MS", String(60 * 60 * 1000), 60_000, 24 * 60 * 60 * 1000),
|
|
868
896
|
};
|
|
897
|
+
if (cfg.windowMs < 0) {
|
|
898
|
+
throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} must not be negative — 0 means "close the window" (every gated ask goes straight to the unattended outcome and NO approval card is emitted), any positive value is the live-card window in ms. A negative value used to emit a card nobody could ever win.`);
|
|
899
|
+
}
|
|
900
|
+
if (cfg.windowMs > MAX_TIMER_DELAY_MS) {
|
|
901
|
+
throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} exceeds the Node timer ceiling ${MAX_TIMER_DELAY_MS} (2^31-1 ms ≈ 24.8 days) — setTimeout SILENTLY collapses a larger delay to 1ms, so the window you meant to lengthen would expire immediately and every gated ask would take the unattended route on the next tick. Pick a window at or below the ceiling (and remember STREAM_ASK_WINDOW_MS + STREAM_APPROVAL_ADHOC_GRACE_MS must stay strictly below STREAM_APPROVAL_ORPHAN_TTL_MS).`);
|
|
902
|
+
}
|
|
869
903
|
if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
|
|
870
904
|
throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
|
|
871
905
|
}
|
|
@@ -945,6 +979,7 @@ function parseApprovalDomain(ctx) {
|
|
|
945
979
|
`The words are matched EXACTLY (lowercase, surrounding whitespace trimmed): "OFF"/"Classify" land here too, deliberately — a closed word table with no case-folding is what keeps "it looked accepted" from meaning "it did nothing". ` +
|
|
946
980
|
`Refusing to boot rather than treating a misspelled value as "unset": that silently ran the deployment with NO shell gate while the operator believed one was configured`);
|
|
947
981
|
})(process.env.MANUAL_MODE_SHELL_GATE),
|
|
982
|
+
permissionsDisableAutoMode: boolEnv("PERMISSIONS_DISABLE_AUTO_MODE", false),
|
|
948
983
|
mcpElicitation: {
|
|
949
984
|
enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
|
|
950
985
|
throttle: {
|
|
@@ -1757,7 +1792,7 @@ const APPROVAL_GROUP_KEYS = [
|
|
|
1757
1792
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
1758
1793
|
"approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
1759
1794
|
"resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
|
|
1760
|
-
"sensitiveWritePatterns", "manualModeShellGate",
|
|
1795
|
+
"sensitiveWritePatterns", "manualModeShellGate", "permissionsDisableAutoMode",
|
|
1761
1796
|
];
|
|
1762
1797
|
const MEMORY_GROUP_KEYS = [
|
|
1763
1798
|
"memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
|
|
@@ -673,7 +673,7 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
|
|
|
673
673
|
if (!out)
|
|
674
674
|
continue;
|
|
675
675
|
if (typeof out.systemMessage === "string")
|
|
676
|
-
ctx.logger.
|
|
676
|
+
ctx.logger.info("hook_system_message", { event, message: clip(out.systemMessage, 500) });
|
|
677
677
|
if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
|
|
678
678
|
ctx.logger.warn("hook_result_unsupported", { event });
|
|
679
679
|
}
|
|
@@ -747,7 +747,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
747
747
|
if (!out)
|
|
748
748
|
continue;
|
|
749
749
|
if (typeof out.systemMessage === "string")
|
|
750
|
-
ctx.logger.
|
|
750
|
+
ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
|
|
751
751
|
if (out.continue === false) {
|
|
752
752
|
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
753
753
|
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
@@ -845,7 +845,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
845
845
|
if (!out)
|
|
846
846
|
continue;
|
|
847
847
|
if (typeof out.systemMessage === "string")
|
|
848
|
-
ctx.logger.
|
|
848
|
+
ctx.logger.info("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
|
|
849
849
|
if (out.continue === false) {
|
|
850
850
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
|
|
851
851
|
}
|
|
@@ -895,7 +895,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
895
895
|
if (!out)
|
|
896
896
|
continue;
|
|
897
897
|
if (typeof out.systemMessage === "string")
|
|
898
|
-
ctx.logger.
|
|
898
|
+
ctx.logger.info("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
|
|
899
899
|
if (out.continue === false)
|
|
900
900
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
|
|
901
901
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
@@ -942,7 +942,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
942
942
|
if (!out)
|
|
943
943
|
continue;
|
|
944
944
|
if (typeof out.systemMessage === "string")
|
|
945
|
-
ctx.logger.
|
|
945
|
+
ctx.logger.info("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
|
|
946
946
|
if (out.continue === false)
|
|
947
947
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
|
|
948
948
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
@@ -974,7 +974,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
974
974
|
if (!out)
|
|
975
975
|
continue;
|
|
976
976
|
if (typeof out.systemMessage === "string")
|
|
977
|
-
ctx.logger.
|
|
977
|
+
ctx.logger.info("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
|
|
978
978
|
if (out.continue === false) {
|
|
979
979
|
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
980
980
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
@@ -1063,7 +1063,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
1063
1063
|
continue;
|
|
1064
1064
|
}
|
|
1065
1065
|
if (typeof out.systemMessage === "string")
|
|
1066
|
-
ctx.logger.
|
|
1066
|
+
ctx.logger.info("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
|
|
1067
1067
|
if (out.continue === false)
|
|
1068
1068
|
sawContinueFalse = true;
|
|
1069
1069
|
if (out.decision === "block" && block === undefined) {
|
|
@@ -1122,7 +1122,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
1122
1122
|
if (!out)
|
|
1123
1123
|
continue;
|
|
1124
1124
|
if (typeof out.systemMessage === "string")
|
|
1125
|
-
ctx.logger.
|
|
1125
|
+
ctx.logger.info("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
|
|
1126
1126
|
if (out.continue === false) {
|
|
1127
1127
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
|
|
1128
1128
|
}
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -47,6 +47,24 @@ export interface RouteHelpers {
|
|
|
47
47
|
rateLimited(req: IncomingMessage, res: ServerResponse): boolean;
|
|
48
48
|
quotaExceeded(req: IncomingMessage, res: ServerResponse): boolean;
|
|
49
49
|
leaseDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* 🔴 **[ref]:按**付费**方(不是请求方)判的成本配额准入 —— {@link quotaExceeded} 的孪生。**
|
|
52
|
+
*
|
|
53
|
+
* `quotaExceeded` 键在 `gatedPrincipal(req)` = 谁点的按钮;本门键在**这笔钱记谁的账**。两者在自助
|
|
54
|
+
* 场景恒相等,在「显式 operator 代别人动手」的场景不等 —— 那正是配额上限被绕过的那一支。
|
|
55
|
+
* 参数 `owner` 由调用点从**已属主校验过的那一行**取(run 行的 `owner` / checkpoint 的 scope),
|
|
56
|
+
* 本门不自己推身份(推得出来就说明调用点还有第二个身份源,那才是病)。
|
|
57
|
+
* `undefined`(匿名/开放行)⇒ 无付费方 ⇒ 不 gated,与 `quotaExceeded` 的成文语义同判。
|
|
58
|
+
* 拒 = 429 + `Retry-After`,码/文案/extras 与 `quotaExceeded` 逐字同一套。
|
|
59
|
+
* 位序纪律:**必须排在本路由的 owner 门之后**(反枚举 —— 否则 429 会变成「这行存在且它属主超顶了」
|
|
60
|
+
* 的谕示);且排在真正驱动模型之前。
|
|
61
|
+
*/
|
|
62
|
+
ownerQuotaDenied(res: ServerResponse, owner: string | undefined): boolean;
|
|
63
|
+
/** 🔴 **[ref]:{@link ownerQuotaDenied} 的 lease 孪生 —— 按**付费**方判的 fleet-lease 准入。**
|
|
64
|
+
* `leaseDenied` 键在请求方;本门键在付费方(`driveResumeLeg` 的 `fleetLease.admit(卡属主)` 早就是这个
|
|
65
|
+
* 语义,本位把它交给路由域)。两门在自助场景恒同答,在「operator 代别人动手」时不同 —— 那正是
|
|
66
|
+
* 「拿自己的额度去烧别人的账」的那一支。同样必须排在 owner 门之后、驱动之前。 */
|
|
67
|
+
ownerLeaseDenied(res: ServerResponse, owner: string | undefined): Promise<boolean>;
|
|
50
68
|
/** [ref]-T1:治理窗 pre-admission(异步 202 车道的唯一 429 出路;key 公式与 core 单源)。 */
|
|
51
69
|
usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
52
70
|
safeDecode(seg: string): string | null;
|
|
@@ -79,6 +97,17 @@ export interface RouteRequestState {
|
|
|
79
97
|
streamDetached: boolean;
|
|
80
98
|
/** 凭证派生的调用方系统身份(`source`)。handle() 的鉴权门是**唯一**赋值点,域模块只读。 */
|
|
81
99
|
source: string | null;
|
|
100
|
+
/**
|
|
101
|
+
* [ref] 件2(cli [ref] sema-bug4 (c)):本请求驱动的那条 run 的**终局**(`completed` / `failed` /
|
|
102
|
+
* `suspended` / …)与结构化失败码。`handle()` 的 `request` 日志行读它 —— 让「端点 200 ≠ 模型成功」
|
|
103
|
+
* 这条歧义消失(取证:模型 connect timeout 的那一刻,日志里唯一的行是 `request status:200`)。
|
|
104
|
+
*
|
|
105
|
+
* 赋值点在 `routes/tasks.ts`(`/v1/tasks` 与 `/v1/tasks/stream` 两腿的终局到手处);拿不到 run 结果
|
|
106
|
+
* (受理即拒 / 幂等重放 / 非任务端点)⇒ **键缺席**,绝不写 null —— 缺席 = 「这条请求没驱动 run」,
|
|
107
|
+
* 与「run 结果不明」是两句话,而 `null` 会把两者压成一个形。
|
|
108
|
+
*/
|
|
109
|
+
runStatus?: string;
|
|
110
|
+
runErrorCode?: string;
|
|
82
111
|
}
|
|
83
112
|
/** B4①(命名化):`prepareSpec` 的返回形(tasks.ts/runs.ts 两域共同消费的属性面)——原为 6 成员内联匿名对象。
|
|
84
113
|
* 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
|
|
@@ -141,6 +170,52 @@ export interface DriveResumeArgs {
|
|
|
141
170
|
* **唯一不设此位的**是内部 D-D SLA deny-sweep:它没有在等响应的人,提前受理对它零收益,而且那条
|
|
142
171
|
* 循环靠 `await` 串行给每 tick 的重活限流(见其调用处的登记注)。 */
|
|
143
172
|
acceptEarly?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* 🔴 **[ref](codex 交叉复审 r2-[medium] 验真后修):这条腿是**调用方发起**的吗?**
|
|
175
|
+
*
|
|
176
|
+
* `true` = 有人在 HTTP 上点了按钮(resume / plan_review / wake / decide / 赎回席)⇒ 吃按**付费方**
|
|
177
|
+
* (卡属主)判的成本配额准入(`ownerCostQuotaDenied`)。缺席/`false` = **部署自己的结算腿**
|
|
178
|
+
* (内部 D-D SLA deny-sweep)⇒ **不吃**那道门。
|
|
179
|
+
*
|
|
180
|
+
* 为什么系统腿必须豁免(亲核后改的判据,首版曾一并拦下 sweep):sweep 的活是把过了 SLA 的卡**结算掉**
|
|
181
|
+
* (graceful deny → 行收敛 → 释放 `task_active` claim)。拦住它并不能省钱,只会让超顶租户的卡**挂着**
|
|
182
|
+
* —— 而挂多久的兜底**比首版注释里写的差得多**:`checkpoint-store-sql.ts` 的 `reapExpired` 在
|
|
183
|
+
* **deadline 分支**上逐字排除了 `APPROVAL_GATE_KINDS`(`gate_kind NOT IN (…) OR tool_name='AskUserQuestion'`),
|
|
184
|
+
* 所以一张过期的 human / irreversible_ask 卡根本不走那条腿;真正兜住它的只有绝对 backstop
|
|
185
|
+
* `terminal_at_ms`(`TERMINAL_BACKSTOP_MS` 默认 **30 天**)。于是「拦 sweep」= 把一个由
|
|
186
|
+
* `APPROVAL_TIMEOUT_SEC` 界定的 SLA 换成「等成本窗滚过(`COST_QUOTA_WINDOW_SEC` 默认 24h)、最坏等 30 天」,
|
|
187
|
+
* 并且整段时间攥着会话 claim。**方向也自洽**:deny 是**停下**这条 run 的动作,为了省钱而拒绝执行「停」
|
|
188
|
+
* 与 preempt 路由早就成文的那句同一条(「否则超顶租户连自己的花费都停不下来」)。
|
|
189
|
+
*
|
|
190
|
+
* 判别子取 `req` 的在场与否 —— 这在 `resumeCheckpoint` 里**已经是**「系统腿 vs 调用方腿」的既有判据
|
|
191
|
+
* (绑定强制、parked 分流两处都写着 `req !== undefined` 与「内部 D-D SLA deny-sweep(req 缺席)豁免」),
|
|
192
|
+
* 本位只是把同一条判据显式化成一个有名字的位,而不是让下游去猜。
|
|
193
|
+
* ⚠️ **射程覆盖两道门**:紧邻的 fleet-lease 准入(`ownerFleetLeaseDenied`)读**同一位** —— [ref] 件⑤②
|
|
194
|
+
* (codex r3-[high])把它一并改吃了 `callerInitiated`,理由与三问逐字同 cost 半场。
|
|
195
|
+
* 🔴 本句是 [ref] R4 改写的(2026-09-03,7.57.0 发车前合并重扫 wf_1abaaa9a):此处原文写着「紧邻的
|
|
196
|
+
* fleet-lease 准入**没有**这条豁免(对 sweep 一视同仁),本批不动它,分歧已登记为遗留」—— 那是**同一批
|
|
197
|
+
* 件⑤② 落地之前**的话,件⑤② 修好后这条 JSDoc 没跟着改,于是注释与代码正相反(亲读
|
|
198
|
+
* `server.ts` 的 `ownerFleetLeaseDenied` 消费点:两处都是 `args.callerInitiated === true` /
|
|
199
|
+
* `req !== undefined`)。两道门在这一位上**无分歧**,[ref] 上也没有这一项。
|
|
200
|
+
*/
|
|
201
|
+
callerInitiated?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* 🔴 **[ref] codex 对抗复审 r1-F3 [medium](验真后修):本次请求的属主 lease 准入**已经做过了**。**
|
|
204
|
+
*
|
|
205
|
+
* `resumeCheckpoint` 在 parked 子代赎回分流**之前**先判一次属主 lease([ref] R3:那条腿提前 `return`,
|
|
206
|
+
* 结构上够不到 `driveResumeLeg` 的同一道门)。分流**未命中**时请求落回 legacy 腿,于是同一条请求、
|
|
207
|
+
* 同一个付费方会向 center 协商**两次**。cost 半场可以「过两次门」(本地同步、纯读);lease 半场**不行**:
|
|
208
|
+
* `FleetLeaseManager.admit` 的五条 fail-open 路径(center 不可达 / 5xx / 超时 / 401-403 / 畸形响应)
|
|
209
|
+
* 一律**不写缓存态**(亲读 `fleet-lease.ts` 的 `issueOrRenewInner`),而 single-flight 只合并**并发**调用 ——
|
|
210
|
+
* 两道门是串行的 ⇒ 半开 center 下一个请求连吃两次 `LEASE_FETCH_TIMEOUT_MS`,持续故障时整条 legacy
|
|
211
|
+
* resume 车道把控制面负载翻倍。
|
|
212
|
+
*
|
|
213
|
+
* 所以由上游把「已受理」这个事实传下来,`driveResumeLeg` 据此跳过重复协商。**安全性**:两处的键是
|
|
214
|
+
* **同一个表达式**(`decodeCheckpointScope(cp.scope)`,上游门与下游 `auth.principal` 同源),射程条件也
|
|
215
|
+
* 同一条(`req !== undefined` ⇔ `callerInitiated`)⇒ 跳过的是一次**逐字相同**的判定,不是一道门。
|
|
216
|
+
* 缺席/`false` ⇒ 照常自己判(其余三条驱动腿与 sweep 一字不变)。
|
|
217
|
+
*/
|
|
218
|
+
ownerLeaseAdmitted?: boolean;
|
|
144
219
|
}
|
|
145
220
|
/** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
|
|
146
221
|
* run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal-log.js";
|
|
2
3
|
import { A2A_TASK_STATES } from "@sema-agent/core";
|
|
3
|
-
import { isParkedRunStatus } from "../../plugins/store-contracts.js";
|
|
4
|
+
import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
|
|
4
5
|
import { HttpError } from "../../security.js";
|
|
5
6
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
6
7
|
import { sendJson, sendError } from "../send.js";
|
|
@@ -80,10 +81,11 @@ function statusTextForRun(run, state) {
|
|
|
80
81
|
return "this task is suspended awaiting an approval decision on the serving deployment; it resumes once that decision is made";
|
|
81
82
|
if (run.result?.result)
|
|
82
83
|
return run.result.result;
|
|
84
|
+
const memo = (seat, raw) => (isTerminalRunStatus(run.status) ? memoRedactedRead(`a2a.${seat}`, raw, (v) => redactErrorMessage(v) ?? "") : (redactErrorMessage(raw) ?? ""));
|
|
83
85
|
if (run.error)
|
|
84
|
-
return run.error;
|
|
86
|
+
return memo("error", run.error);
|
|
85
87
|
if (run.result?.errorMessage)
|
|
86
|
-
return run.result.errorMessage;
|
|
88
|
+
return memo("errorMessage", run.result.errorMessage);
|
|
87
89
|
return undefined;
|
|
88
90
|
}
|
|
89
91
|
function taskFromRun(run, stale) {
|
|
@@ -107,6 +107,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
107
107
|
: (principal ?? "__none__");
|
|
108
108
|
sendJson(res, 200, {
|
|
109
109
|
pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
|
|
110
|
+
serverNowMs: Date.now(),
|
|
110
111
|
...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
|
|
111
112
|
...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
|
|
112
113
|
});
|
|
@@ -520,9 +521,20 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
520
521
|
const APPROVALS_STREAM_POLL_MS = 3000;
|
|
521
522
|
function redactPendingDisclosures(row) {
|
|
522
523
|
const rd = row.riskDescriptor;
|
|
523
|
-
if (rd === null || rd === undefined
|
|
524
|
+
if (rd === null || rd === undefined)
|
|
524
525
|
return row;
|
|
525
|
-
|
|
526
|
+
const shadowedRule = typeof rd.shadowedRule === "string" ? redactSecrets(rd.shadowedRule) : undefined;
|
|
527
|
+
const summary = typeof rd.summary === "string" ? redactSecrets(rd.summary) : undefined;
|
|
528
|
+
if (shadowedRule === undefined && summary === undefined)
|
|
529
|
+
return row;
|
|
530
|
+
return {
|
|
531
|
+
...row,
|
|
532
|
+
riskDescriptor: {
|
|
533
|
+
...rd,
|
|
534
|
+
...(shadowedRule !== undefined ? { shadowedRule } : {}),
|
|
535
|
+
...(summary !== undefined ? { summary } : {}),
|
|
536
|
+
},
|
|
537
|
+
};
|
|
526
538
|
}
|
|
527
539
|
function projectPendingForWire(row, governance) {
|
|
528
540
|
const redacted = redactPendingDisclosures(row);
|
|
@@ -553,19 +565,20 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
|
|
|
553
565
|
}
|
|
554
566
|
const cur = new Map(pending.map((p) => [keyOf(p), p]));
|
|
555
567
|
const curFrames = new Map([...cur].map(([k, p]) => [k, JSON.stringify({ type: "pending", ...p })]));
|
|
568
|
+
const pendingFrame = (k) => JSON.stringify({ type: "pending", ...cur.get(k), serverNowMs: Date.now() });
|
|
556
569
|
if (first) {
|
|
557
|
-
for (const
|
|
558
|
-
res.write(`event: pending\ndata: ${
|
|
570
|
+
for (const k of curFrames.keys())
|
|
571
|
+
res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
|
|
559
572
|
res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
|
|
560
573
|
first = false;
|
|
561
574
|
}
|
|
562
575
|
else {
|
|
563
576
|
for (const [k, frame] of curFrames)
|
|
564
577
|
if (prevFrames.get(k) !== frame)
|
|
565
|
-
res.write(`event: pending\ndata: ${
|
|
578
|
+
res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
|
|
566
579
|
for (const [k, p] of prev)
|
|
567
580
|
if (!cur.has(k))
|
|
568
|
-
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
|
|
581
|
+
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId, serverNowMs: Date.now() })}\n\n`);
|
|
569
582
|
}
|
|
570
583
|
prev = cur;
|
|
571
584
|
prevFrames = curFrames;
|
|
@@ -9,8 +9,10 @@ import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
|
9
9
|
import { sendJson, sendError } from "../send.js";
|
|
10
10
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
11
11
|
import { projectMemoryEngineCapability } from "../../memory-posture.js";
|
|
12
|
-
import { workflowModelAllowlistFor } from "../../task-workflow.js";
|
|
12
|
+
import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
|
|
13
13
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
14
|
+
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
15
|
+
import { coercePermissionModeStrict } from "../../task-settings.js";
|
|
14
16
|
import { gatedPrincipal } from "../principal-gate.js";
|
|
15
17
|
function routePairingOf(deps, name, m) {
|
|
16
18
|
if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
|
|
@@ -43,6 +45,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
if (req.method === "GET" && url === "/v1/capabilities") {
|
|
48
|
+
const workflowsEngineCan = deps.workflowsCapable ?? Boolean(deps.workflowRunStore);
|
|
49
|
+
const workflowsDenial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
|
|
50
|
+
const requestedModeRaw = new URL(req.url ?? "", "http://x").searchParams.get("permissionMode");
|
|
51
|
+
const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
|
|
52
|
+
if (requestedModeRaw !== null && requestedMode === undefined) {
|
|
53
|
+
sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const entitlementSource = centerEntitlementSourceWired(deps.config);
|
|
57
|
+
const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), entitlementSource, localDeny: deps.config.permissionsDisableAutoMode });
|
|
58
|
+
if (autoModeFault !== undefined)
|
|
59
|
+
deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
|
|
46
60
|
sendJson(res, 200, {
|
|
47
61
|
...deps.capabilities,
|
|
48
62
|
asyncRuns: Boolean(deps.runStore),
|
|
@@ -92,8 +106,10 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
92
106
|
modeShellGateTranslation: true,
|
|
93
107
|
permissionModeAuto: {
|
|
94
108
|
accepted: true,
|
|
95
|
-
classifierSeat: true,
|
|
96
|
-
entitlementSource
|
|
109
|
+
classifierSeat: deps.autoModeFace?.seatMounted === true,
|
|
110
|
+
entitlementSource,
|
|
111
|
+
intentArming: deps.autoModeFace?.engineArmsOnIntent === true,
|
|
112
|
+
...autoMode,
|
|
97
113
|
},
|
|
98
114
|
readFace: deps.config.readFace ?? null,
|
|
99
115
|
appendSystemPrompt: true,
|
|
@@ -141,8 +157,9 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
141
157
|
sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
|
|
142
158
|
s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
|
|
143
159
|
sendUserFileLedger: Boolean(deps.sendFileLedger),
|
|
144
|
-
workflows:
|
|
160
|
+
workflows: workflowsEngineCan && workflowsDenial === null,
|
|
145
161
|
workflowsList: Boolean(deps.workflowRunStore),
|
|
162
|
+
workflowsGate: { engineCan: workflowsEngineCan, denial: workflowsDenial },
|
|
146
163
|
workflowModels: (() => {
|
|
147
164
|
const allow = deps.workflowModelAllowlist
|
|
148
165
|
?? workflowModelAllowlistFor({
|
|
@@ -99,7 +99,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
99
99
|
detail?: string | undefined;
|
|
100
100
|
eventId: string;
|
|
101
101
|
at: number;
|
|
102
|
-
to: "
|
|
102
|
+
to: "done" | "failed";
|
|
103
103
|
requestId: string;
|
|
104
104
|
}[];
|
|
105
105
|
custodyBytes: number;
|
|
@@ -107,7 +107,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
107
107
|
requestId: string;
|
|
108
108
|
reason: string;
|
|
109
109
|
at: number;
|
|
110
|
-
status: "pending" | "
|
|
110
|
+
status: "pending" | "done" | "failed";
|
|
111
111
|
originUnknownKeys?: string[] | undefined;
|
|
112
112
|
clearanceId: string;
|
|
113
113
|
entryId: string;
|
|
@@ -6,6 +6,7 @@ import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, seri
|
|
|
6
6
|
import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
|
|
7
7
|
import { HttpError } from "../../security.js";
|
|
8
8
|
import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
9
|
+
import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
|
|
9
10
|
const logger = createLogger();
|
|
10
11
|
export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
11
12
|
export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
@@ -45,8 +46,8 @@ const LocalImportRowSchema = z
|
|
|
45
46
|
.object({
|
|
46
47
|
rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
|
|
47
48
|
scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
|
|
48
|
-
tool: z.
|
|
49
|
-
match: z.enum(
|
|
49
|
+
tool: z.enum(PERSISTED_RULE_TOOLS),
|
|
50
|
+
match: z.enum(PERSISTED_RULE_MATCHES),
|
|
50
51
|
command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
|
|
51
52
|
adds: z.array(LocalImportAddSchema).max(64),
|
|
52
53
|
})
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* [ref] A9 域模块 —— 异步 run 面与它的全部动词。
|
|
3
|
-
*
|
|
4
|
-
* 路由:`POST /v1/runs`(异步提交)· `GET /v1/runs/:id[/events]`(轮询 / 可恢复 SSE)· 运行期动词
|
|
5
|
-
* `cancel` / `steer` / `compact` / `detach` · 子代理面 `subagents/:target/{steer,resume,output,stream}` ·
|
|
6
|
-
* 通用任务句柄 `tasks/:handle/{output,stop}` · 三条 HITL 应答口 `elicitations|questions|tool-approvals/:id/respond`。
|
|
7
|
-
*
|
|
8
|
-
* 两段搬运:提交+run 级动词一段,子代理/任务句柄/HITL 应答一段——它们在 `handle()` 里原本被 SVC-5 的
|
|
9
|
-
* workflow steer 隔开,拆分保持各自位置(本文件里的先后只是同域归并)。`POST /v1/runs` 与 tasks 域共用
|
|
10
|
-
* `ctx.legs.prepareSpec`(同一条读体/校验/建 spec 的前段)。
|
|
11
|
-
*
|
|
12
|
-
* 可变状态全部来自跨域 registry(`idemCache` 幂等、`inflightRuns`/`preemptableRuns` 中止句柄、
|
|
13
|
-
* `cancelledViaVerb` first-writer-wins 判别、`steerableRuns` 活流句柄、`wakeParkMints` park 铸造闩)。
|
|
14
|
-
*/
|
|
15
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
2
|
import { ActorAssertionWire } from "@sema-agent/settings-schema";
|
|
17
3
|
import type { RouteCtx, PreparedTaskSubmission } from "../route-ctx.js";
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal-log.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
|
|
3
4
|
import { isTerminalRunStatus, isParkedRunStatus } from "../../plugins/store-contracts.js";
|
|
@@ -22,10 +23,12 @@ import { normalizeRunEventType } from "../../trace/project.js";
|
|
|
22
23
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
23
24
|
import { buildActiveRunConflict } from "../active-run-conflict.js";
|
|
24
25
|
import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
|
|
26
|
+
import { buildCancelContext } from "../../run-cancel-context.js";
|
|
25
27
|
import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
|
|
26
28
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
27
29
|
import { ActorAssertionWire } from "@sema-agent/settings-schema";
|
|
28
30
|
import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
|
|
31
|
+
import { withPrincipal } from "../../observability/principal-context.js";
|
|
29
32
|
export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
|
|
30
33
|
export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
|
|
31
34
|
export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
|
|
@@ -136,7 +139,10 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
|
|
|
136
139
|
retainedFrom: (id) => runStore.retainedFrom(id),
|
|
137
140
|
formatEvent: (ev) => {
|
|
138
141
|
const type = normalizeRunEventType(ev.type);
|
|
139
|
-
|
|
142
|
+
const data = type === "done" && ev.data && typeof ev.data === "object" && ev.data.result && typeof ev.data.result === "object"
|
|
143
|
+
? { ...ev.data, result: memoRedactTerminalResult("ev.done.result", ev.data.result) }
|
|
144
|
+
: type === "failed" ? memoRedactedRead("ev.failed", ev.data, (d) => redactLedgerEventData(type, d)) : redactLedgerEventData(type, ev.data);
|
|
145
|
+
return { id: ev.seq, event: type, data: { type, ...(data ?? {}) } };
|
|
140
146
|
},
|
|
141
147
|
staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
|
|
142
148
|
}, taskId, staleMs);
|
|
@@ -251,11 +257,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
251
257
|
status: stale ? "failed" : run.status,
|
|
252
258
|
...(lastActivityAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastActivityAt) } : {}),
|
|
253
259
|
...(crossSliceUsage !== undefined ? { crossSliceUsage } : {}),
|
|
254
|
-
result: run.result ?? undefined,
|
|
260
|
+
result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : redactTerminalResult(run.result ?? undefined),
|
|
255
261
|
supervisorCost,
|
|
256
262
|
suggestions,
|
|
257
263
|
errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
|
|
258
|
-
error: stale ? "run stalled (instance lost?)" : run.error ?? undefined,
|
|
264
|
+
error: stale ? "run stalled (instance lost?)" : isTerminalRunStatus(run.status) ? memoRedactedRead("run.error", run.error ?? undefined, redactErrorMessage) : redactErrorMessage(run.error ?? undefined),
|
|
259
265
|
jobId: run.jobId ?? undefined,
|
|
260
266
|
source: run.source ?? undefined,
|
|
261
267
|
});
|
|
@@ -324,7 +330,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
324
330
|
}
|
|
325
331
|
const err = note;
|
|
326
332
|
try {
|
|
327
|
-
const
|
|
333
|
+
const cancelContext = buildCancelContext(taskId, Date.parse(run.createdAt));
|
|
334
|
+
const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
|
|
328
335
|
let lastErr;
|
|
329
336
|
for (let attempt = 0;; attempt++) {
|
|
330
337
|
try {
|
|
@@ -870,7 +877,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
870
877
|
deps.logger?.[level]?.("manual_compact_outcome", { taskId, outcome, note: outcome === "compacted" ? "a compacted{trigger:'manual'} event rode the run stream" : `no event (see compaction.${outcome === "mooted" ? "mooted" : outcome} trace / task terminal events)` });
|
|
871
878
|
}, (e) => {
|
|
872
879
|
if (e.code !== "steering.not_running") {
|
|
873
|
-
deps.logger?.warn?.("manual_compact_failed", { taskId, err: e instanceof Error ? e.message : String(e) });
|
|
880
|
+
deps.logger?.warn?.("manual_compact_failed", { taskId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
|
|
874
881
|
}
|
|
875
882
|
else {
|
|
876
883
|
deps.logger?.info?.("manual_compact_not_running", { taskId, note: "stream settled between lookup and compact() — benign unless systematic" });
|
|
@@ -934,7 +941,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
934
941
|
}
|
|
935
942
|
async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
936
943
|
const { deps } = ctx;
|
|
937
|
-
const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
|
|
944
|
+
const { readJson, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
|
|
938
945
|
const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
|
|
939
946
|
if (subOutputMatch) {
|
|
940
947
|
if (!deps.runStore || !deps.subagentTaskOutput) {
|
|
@@ -1159,7 +1166,12 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1159
1166
|
send409("resume.retain_off", "the parent run did not retain sub-agent sessions (set retainSubagentSessions on the run to enable revival)");
|
|
1160
1167
|
return;
|
|
1161
1168
|
}
|
|
1162
|
-
|
|
1169
|
+
if (ownerQuotaDenied(res, run.owner ?? undefined))
|
|
1170
|
+
return;
|
|
1171
|
+
if (await ownerLeaseDenied(res, run.owner ?? undefined))
|
|
1172
|
+
return;
|
|
1173
|
+
const resumeFn = handle.resume;
|
|
1174
|
+
const marker = await withPrincipal(run.owner ?? undefined, () => resumeFn(redacted));
|
|
1163
1175
|
sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
|
|
1164
1176
|
return;
|
|
1165
1177
|
}
|
|
@@ -1593,7 +1605,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
|
|
|
1593
1605
|
backend: deps.backend,
|
|
1594
1606
|
parkFacility: deps.checkpointStore !== undefined,
|
|
1595
1607
|
}).active,
|
|
1596
|
-
windowMs: deps.config.streamApproval?.windowMs
|
|
1608
|
+
windowMs: deps.config.streamApproval?.windowMs,
|
|
1597
1609
|
windowMarginMs: deps.config.streamAskWindowMarginMs,
|
|
1598
1610
|
}
|
|
1599
1611
|
: undefined, deps.checkpointStore);
|