@sema-agent/server 7.100.0 → 7.101.1
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 +22 -1
- package/README.md +12 -1
- package/README.zh-CN.md +9 -1
- package/USAGE.md +80 -9
- package/dist/approval-card.d.ts +24 -12
- package/dist/approval-card.js +4 -3
- package/dist/approval-policy-settlement.d.ts +31 -0
- package/dist/approval-policy-settlement.js +16 -0
- package/dist/approval-reconciler.d.ts +8 -1
- package/dist/approval-reconciler.js +5 -4
- package/dist/boot/config-center.js +86 -20
- package/dist/boot/execution-env.js +20 -2
- package/dist/boot/parked-revive-gate.js +1 -0
- package/dist/boot/reapers.js +1 -0
- package/dist/boot/resolve-spec.js +25 -8
- package/dist/boot/runner-deps.js +9 -3
- package/dist/boot/stage-02-registry-pull.js +9 -0
- package/dist/boot/stage-10-http-server.js +2 -0
- package/dist/boot/stores.js +4 -0
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/apply-effective.d.ts +25 -0
- package/dist/config-center/apply-effective.js +144 -69
- package/dist/config-center/apply-ledger.d.ts +12 -2
- package/dist/config-center/apply-ledger.js +4 -1
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +4 -1
- package/dist/config-center/http-client.js +15 -10
- package/dist/config-center/read-warnings.d.ts +115 -6
- package/dist/config-center/read-warnings.js +115 -5
- package/dist/config-center/types.d.ts +6 -0
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +15 -0
- package/dist/config-provider.d.ts +4 -0
- package/dist/config-provider.js +64 -9
- package/dist/config-types.d.ts +37 -8
- package/dist/config.js +87 -22
- package/dist/deployment-governance.d.ts +45 -12
- package/dist/deployment-governance.js +37 -9
- package/dist/env-name-allowlist-knobs.d.ts +37 -0
- package/dist/env-name-allowlist-knobs.js +24 -0
- package/dist/fleet/fleet-bus.d.ts +30 -4
- package/dist/fleet/fleet-bus.js +20 -4
- package/dist/fleet/fleet-reconciler.d.ts +29 -6
- package/dist/fleet/fleet-reconciler.js +109 -79
- package/dist/fleet-client.d.ts +2 -1
- package/dist/hooks/hook-llm.js +13 -4
- package/dist/host-lsp-manager.d.ts +14 -2
- package/dist/host-lsp-manager.js +7 -6
- package/dist/http/admission.js +22 -7
- package/dist/http/memory-store-refusal-reply.d.ts +24 -0
- package/dist/http/memory-store-refusal-reply.js +10 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +3 -3
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +3 -5
- package/dist/http/routes/memory-origin.js +3 -5
- package/dist/http/routes/sessions.js +7 -6
- package/dist/http/routes/side-query.js +10 -4
- package/dist/instruction-source-notice.d.ts +97 -0
- package/dist/instruction-source-notice.js +78 -0
- package/dist/memory-layer-legacy-lock.d.ts +14 -0
- package/dist/memory-layer-legacy-lock.js +74 -0
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-store-refusal.d.ts +37 -0
- package/dist/memory-store-refusal.js +14 -0
- package/dist/model-entry-refusal.d.ts +130 -0
- package/dist/model-entry-refusal.js +157 -0
- package/dist/model-provider.d.ts +9 -3
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-select.d.ts +49 -4
- package/dist/model-select.js +48 -17
- package/dist/observability/fail-open.d.ts +13 -1
- package/dist/observability/fail-open.js +13 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +27 -4
- package/dist/observability/secret-env-scrub.js +22 -7
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/local-session-store.d.ts +20 -25
- package/dist/plugins/local-session-store.js +111 -64
- package/dist/plugins/pg-session-storage.d.ts +7 -0
- package/dist/plugins/pg-session-storage.js +19 -0
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -6
- package/dist/plugins/tidb-pool.js +5 -0
- package/dist/plugins/tidb-session-store.d.ts +13 -0
- package/dist/plugins/tidb-session-store.js +14 -0
- package/dist/plugins/web-search.d.ts +2 -1
- package/dist/plugins/web-search.js +43 -24
- package/dist/project-memory.d.ts +10 -0
- package/dist/project-memory.js +28 -14
- package/dist/run-local.d.ts +17 -15
- package/dist/run-local.js +115 -15
- package/dist/sealed-key.d.ts +5 -2
- package/dist/server-secret-env.d.ts +23 -1
- package/dist/server-secret-env.js +9 -0
- package/dist/tool-approval.d.ts +74 -31
- package/dist/tool-approval.js +102 -49
- package/dist/trace/core-keyset-guard.d.ts +27 -5
- package/dist/trace/engine-notice-wire.d.ts +27 -1
- package/dist/trace/engine-notice-wire.js +8 -0
- package/dist/trace/wire-projection-faces.d.ts +2 -2
- package/dist/trace/wire-projection-faces.js +2 -2
- package/package.json +2 -2
- package/dist/memory-layer-preflight.d.ts +0 -46
- package/dist/memory-layer-preflight.js +0 -121
package/dist/config.js
CHANGED
|
@@ -24,6 +24,8 @@ import { modelProviderRefusal } from "./model-provider.js";
|
|
|
24
24
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
25
25
|
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
26
26
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
27
|
+
import { allowlistedEnv } from "./server-secret-env.js";
|
|
28
|
+
import { ENV_NAME_ALLOWLIST_KNOBS, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
|
|
27
29
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
28
30
|
function csv(name) {
|
|
29
31
|
return (process.env[name] ?? "")
|
|
@@ -84,15 +86,6 @@ export function parseUsageWindows(raw) {
|
|
|
84
86
|
});
|
|
85
87
|
return Object.freeze(out);
|
|
86
88
|
}
|
|
87
|
-
function resolveEnvForward(csvNames) {
|
|
88
|
-
const out = {};
|
|
89
|
-
for (const name of csvNames.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
90
|
-
const v = process.env[name];
|
|
91
|
-
if (v !== undefined && v !== "")
|
|
92
|
-
out[name] = v;
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
89
|
function env(name, fallback) {
|
|
97
90
|
const v = process.env[name];
|
|
98
91
|
if (v === undefined || v === "") {
|
|
@@ -496,6 +489,73 @@ function parseNameListEnv(name, raw, opts) {
|
|
|
496
489
|
}
|
|
497
490
|
return names;
|
|
498
491
|
}
|
|
492
|
+
const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
493
|
+
const ENV_NAME_ALLOWLIST_KNOB_NAMES = new Set(Object.keys(ENV_NAME_ALLOWLIST_KNOBS));
|
|
494
|
+
const ENV_NAME_ALLOWLIST_WITHHELD = "<withheld: entries are not echoed — a value pasted in place of a name can pass the name check>";
|
|
495
|
+
function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
496
|
+
const raw = process.env[knob];
|
|
497
|
+
if (raw === undefined || raw === "")
|
|
498
|
+
return undefined;
|
|
499
|
+
const spec = ENV_NAME_ALLOWLIST_KNOBS[knob];
|
|
500
|
+
const segments = raw.split(",").map((s) => s.trim());
|
|
501
|
+
segments.forEach((seg, i) => {
|
|
502
|
+
const problem = seg.length === 0
|
|
503
|
+
? "empty"
|
|
504
|
+
: !ENV_VAR_NAME_RE.test(seg)
|
|
505
|
+
? 'not an environment-variable name (letters, digits and "_", not starting with a digit)'
|
|
506
|
+
: ENV_NAME_ALLOWLIST_KNOB_NAMES.has(seg.toUpperCase())
|
|
507
|
+
? `the name of a name-list knob (${[...ENV_NAME_ALLOWLIST_KNOB_NAMES].join(" / ")}: their raw text is withheld from every child process and sandbox; naming one would put that text back)`
|
|
508
|
+
: undefined;
|
|
509
|
+
if (problem === undefined)
|
|
510
|
+
return;
|
|
511
|
+
throw new Error(`env ${knob}: entry #${i + 1} of ${segments.length} is ${problem}` +
|
|
512
|
+
" — refusing to start rather than silently allowing a partial list (#157). The value is not echoed (a pasted NAME=value pair may carry a secret)." +
|
|
513
|
+
` Write ${knob}=NAME,NAME with bare variable names, e.g. ${spec.example}.`);
|
|
514
|
+
});
|
|
515
|
+
if (!singleUserTurnkey) {
|
|
516
|
+
const posture = spec.multiTenant;
|
|
517
|
+
switch (posture.kind) {
|
|
518
|
+
case "ignored":
|
|
519
|
+
CONFIG_WARNINGS.push({
|
|
520
|
+
env: knob,
|
|
521
|
+
raw: ENV_NAME_ALLOWLIST_WITHHELD,
|
|
522
|
+
kind: "ineffective",
|
|
523
|
+
detail: `REQUIRE_PRINCIPAL=true (multi-tenant / hosted posture): the named allowlist (${segments.length} entr${segments.length === 1 ? "y" : "ies"}) is ignored — ${posture.why}`,
|
|
524
|
+
});
|
|
525
|
+
return [];
|
|
526
|
+
case "honored":
|
|
527
|
+
CONFIG_NOTICES.push({
|
|
528
|
+
event: "config_env_forwarded_multi_tenant",
|
|
529
|
+
fields: {
|
|
530
|
+
env: knob,
|
|
531
|
+
entries: String(segments.length),
|
|
532
|
+
note: `this worker-level allowlist is forwarded as configured to every tenant (deployer-explicit channel); ${describeAllowlistPostures()}`,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
break;
|
|
536
|
+
default: {
|
|
537
|
+
const unreachable = posture;
|
|
538
|
+
throw new Error(`unhandled name-list posture: ${String(unreachable)}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const present = allowlistedEnv(segments, process.env);
|
|
543
|
+
const resolved = [];
|
|
544
|
+
segments.forEach((name, i) => {
|
|
545
|
+
if (Object.hasOwn(present, name)) {
|
|
546
|
+
if (!resolved.includes(name))
|
|
547
|
+
resolved.push(name);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
CONFIG_WARNINGS.push({
|
|
551
|
+
env: knob,
|
|
552
|
+
raw: ENV_NAME_ALLOWLIST_WITHHELD,
|
|
553
|
+
kind: "entry-rejected",
|
|
554
|
+
detail: `entry #${i + 1} of ${segments.length}: no variable of that name in this process's environment — nothing to pass on (not echoed: a value pasted in place of a name, e.g. ${knob}=$NPM_TOKEN, can pass the name check)`,
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
return resolved;
|
|
558
|
+
}
|
|
499
559
|
function assertReadDenyBuiltinsResolvable(tiers, exclude) {
|
|
500
560
|
const check = (which, config) => {
|
|
501
561
|
try {
|
|
@@ -802,11 +862,6 @@ export function isRosterGateArmed(configCenter) {
|
|
|
802
862
|
function parseModelDomain() {
|
|
803
863
|
const gatewayBaseUrl = firstSetEnv("MODEL_GATEWAY_BASEURL");
|
|
804
864
|
const modelId = env("MODEL_ID", "");
|
|
805
|
-
if (modelId === "") {
|
|
806
|
-
throw new Error("MODEL_ID is required (no factory default since server 3.0.0) — set MODEL_ID=<model name your gateway serves>, " +
|
|
807
|
-
"or configure models via the sema-registry catalog; the old silent default was an internal-only model name " +
|
|
808
|
-
"that made every external deployment fail far from the cause (gateway 400)");
|
|
809
|
-
}
|
|
810
865
|
const explicitProvider = process.env.MODEL_PROVIDER;
|
|
811
866
|
if (explicitProvider !== undefined && explicitProvider !== "") {
|
|
812
867
|
const refusal = modelProviderRefusal(explicitProvider);
|
|
@@ -874,7 +929,13 @@ function parseModelDomain() {
|
|
|
874
929
|
...(extraBody ? { extraBody } : {}),
|
|
875
930
|
};
|
|
876
931
|
const cheapId = process.env.MODEL_CHEAP_ID;
|
|
877
|
-
|
|
932
|
+
if (cheapId && modelId === "") {
|
|
933
|
+
CONFIG_NOTICES.push({
|
|
934
|
+
event: "model_cheap_slot_without_main",
|
|
935
|
+
fields: { hint: "MODEL_CHEAP_ID is set but MODEL_ID is not — the env cheap slot derives from the env main model, so it is not minted; the catalog (config.d/models.json or the config center) serves the roles. Set MODEL_ID to use the env slots, or unset MODEL_CHEAP_ID to silence this line." },
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
const cheapModel = cheapId && modelId !== ""
|
|
878
939
|
? {
|
|
879
940
|
...model,
|
|
880
941
|
id: cheapId,
|
|
@@ -893,7 +954,7 @@ function parseModelDomain() {
|
|
|
893
954
|
}
|
|
894
955
|
: undefined;
|
|
895
956
|
const mainAct = optFinitePositiveEnv("MODEL_AUTO_COMPACT_TOKENS");
|
|
896
|
-
if (applyAutoCompactWindow(model, mainAct) === "removed-1m") {
|
|
957
|
+
if (applyAutoCompactWindow(model, mainAct) === "removed-1m" && modelId !== "") {
|
|
897
958
|
CONFIG_NOTICES.push({
|
|
898
959
|
event: "auto_compact_window_not_derived_1m",
|
|
899
960
|
fields: {
|
|
@@ -903,7 +964,8 @@ function parseModelDomain() {
|
|
|
903
964
|
},
|
|
904
965
|
});
|
|
905
966
|
}
|
|
906
|
-
|
|
967
|
+
if (modelId !== "")
|
|
968
|
+
noticeAutoCompactAboveWindow("main", "MODEL_AUTO_COMPACT_TOKENS", model);
|
|
907
969
|
if (cheapModel) {
|
|
908
970
|
const cheapAct = optFinitePositiveEnv("MODEL_CHEAP_AUTO_COMPACT_TOKENS");
|
|
909
971
|
if (applyAutoCompactWindow(cheapModel, cheapAct) === "removed-1m") {
|
|
@@ -919,7 +981,7 @@ function parseModelDomain() {
|
|
|
919
981
|
}
|
|
920
982
|
noticeAutoCompactAboveWindow("cheap", "MODEL_CHEAP_AUTO_COMPACT_TOKENS", cheapModel);
|
|
921
983
|
}
|
|
922
|
-
const models = { default: model, [model.id]: model };
|
|
984
|
+
const models = modelId === "" ? {} : { default: model, [model.id]: model };
|
|
923
985
|
if (cheapModel)
|
|
924
986
|
models[cheapModel.id] = cheapModel;
|
|
925
987
|
const gatewayApiKey = process.env.MODEL_API_KEY;
|
|
@@ -979,6 +1041,7 @@ function parseModelDomain() {
|
|
|
979
1041
|
models,
|
|
980
1042
|
modelApiKeyEnv: {},
|
|
981
1043
|
modelApiKeys: {},
|
|
1044
|
+
modelRefusals: {},
|
|
982
1045
|
modelQuotaWeights: {},
|
|
983
1046
|
tiers: {},
|
|
984
1047
|
projects: {},
|
|
@@ -1650,6 +1713,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1650
1713
|
}
|
|
1651
1714
|
return boolEnv("LSP_HOST_ENABLED", true);
|
|
1652
1715
|
})();
|
|
1716
|
+
const lspEnvAllow = parseEnvNameAllowlist("LSP_ENV_ALLOW", singleUserTurnkey) ?? [];
|
|
1653
1717
|
{
|
|
1654
1718
|
const lane = process.env.REMOTE_EXEC;
|
|
1655
1719
|
if (lane !== undefined && lane !== "") {
|
|
@@ -1698,7 +1762,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1698
1762
|
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("E2B_TIMEOUT_MS")),
|
|
1699
1763
|
...((n) => (n !== undefined ? { livenessMs: n } : {}))(optNumEnvStrict("E2B_LIVENESS_MS")),
|
|
1700
1764
|
...(process.env.E2B_ALLOW_NET ? { allowInternetAccess: boolEnv("E2B_ALLOW_NET", true) } : {}),
|
|
1701
|
-
...(
|
|
1765
|
+
...((names) => (names !== undefined ? { sandboxEnv: allowlistedEnv(names, process.env) } : {}))(parseEnvNameAllowlist("E2B_SANDBOX_ENV", singleUserTurnkey)),
|
|
1702
1766
|
...(process.env.E2B_MOUNT_PATH ? { mountPath: process.env.E2B_MOUNT_PATH } : {}),
|
|
1703
1767
|
orphanReclaim: boolEnv("E2B_ORPHAN_RECLAIM", true),
|
|
1704
1768
|
}
|
|
@@ -1773,7 +1837,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1773
1837
|
? { network: enumEnv("DOCKER_NETWORK", "bridge", ["none", "bridge", "host"]) }
|
|
1774
1838
|
: {}),
|
|
1775
1839
|
...((n) => (n !== undefined ? { commandTimeoutMs: n } : {}))(optNumEnvStrict("DOCKER_COMMAND_TIMEOUT_MS")),
|
|
1776
|
-
...(
|
|
1840
|
+
...((names) => (names !== undefined ? { env: allowlistedEnv(names, process.env) } : {}))(parseEnvNameAllowlist("DOCKER_SANDBOX_ENV", singleUserTurnkey)),
|
|
1777
1841
|
}
|
|
1778
1842
|
:
|
|
1779
1843
|
process.env.REMOTE_EXEC === "device"
|
|
@@ -1844,6 +1908,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1844
1908
|
toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
|
|
1845
1909
|
lspEnabled: boolEnv("LSP_ENABLED", false),
|
|
1846
1910
|
lspHostEnabled,
|
|
1911
|
+
lspEnvAllow,
|
|
1847
1912
|
workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false),
|
|
1848
1913
|
imageBakes: {
|
|
1849
1914
|
enabled: boolEnv("IMAGE_BAKES_ENABLED", false),
|
|
@@ -2070,7 +2135,7 @@ const STORE_GROUP_KEYS = [
|
|
|
2070
2135
|
];
|
|
2071
2136
|
const MODEL_PLANE_GROUP_KEYS = [
|
|
2072
2137
|
"gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "envModel", "models",
|
|
2073
|
-
"modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
|
|
2138
|
+
"modelApiKeyEnv", "modelApiKeys", "modelRefusals", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
|
|
2074
2139
|
];
|
|
2075
2140
|
const APPROVAL_GROUP_KEYS = [
|
|
2076
2141
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
@@ -2096,7 +2161,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
2096
2161
|
"workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
|
|
2097
2162
|
"workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
|
|
2098
2163
|
"backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
|
|
2099
|
-
"sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
|
|
2164
|
+
"sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled", "lspEnvAllow",
|
|
2100
2165
|
"imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
|
|
2101
2166
|
];
|
|
2102
2167
|
const LIMITS_HTTP_GROUP_KEYS = [
|
|
@@ -70,8 +70,26 @@ export interface PathAdjudication {
|
|
|
70
70
|
export interface LiveQuestionFace {
|
|
71
71
|
hasLiveContext(): boolean;
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* S-694 —— 审批基线的**席**:这条腿上「一个名单产的 `ask` 由谁来服」。闭集三形,每条腿**显式**说出自己那一形
|
|
75
|
+
* ({@link createApprovalBaselinePolicy} 的席参数必填、按 `kind` 穷举 —— 闭集 +1 = 编译红):
|
|
76
|
+
* · `durable` —— checkpoint suspend/resume 服 ask(HTTP 腿 durable 开、park 赎回腿)。AskUserQuestion 的问答门
|
|
77
|
+
* **只**叠在这一形上(它的 `ask` 语义就是 durable park)。
|
|
78
|
+
* · `live` —— Runner 的活 onAsk 席当场服 ask(run-local 的 `createLocalApprover`:TTY 问 y/N / 无 TTY 当场拒;
|
|
79
|
+
* core `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk` 早退,ask 落回调)。三张名单与 durable 席**同一只**策略。
|
|
80
|
+
* · `absent` —— 没有谁服 ask ⇒ adjudicated allow-all;**只**服务零名单(HTTP 腿「durable 关 ∧ 单用户零意图」)。
|
|
81
|
+
* 带名单进来 = 响亮拒:名单被一条没有席的腿静默吞掉,正是 S-694 的病形。
|
|
82
|
+
*
|
|
83
|
+
* 🔴 由来(S-694):修前席是一个**可选**参数、缺席即 allow-all。run-local 调它时没传 ⇒ 三张名单一张都不进策略,
|
|
84
|
+
* 而这条腿其实**有**一只活 onAsk 席 —— [ref] 件一把「没有 durable 赎回腿」等同成了「ask 无处可服」。
|
|
85
|
+
* 席必填之后,「这条腿能不能服 ask」是每个调用点必须写出来的一句话,不再是一个忘了写就成立的缺省。
|
|
86
|
+
*/
|
|
87
|
+
export type ApprovalSeat = DurableApprovalSeat | LiveApprovalSeat | AbsentApprovalSeat;
|
|
88
|
+
/** 席的形名(补偿 warn 等只需要「哪一形」的读者读它,不必铸整只席)。 */
|
|
89
|
+
export type ApprovalSeatKind = ApprovalSeat["kind"];
|
|
90
|
+
/** durable 席:checkpoint suspend/resume 服 ask,问答门叠在名单策略上。 */
|
|
74
91
|
export interface DurableApprovalSeat {
|
|
92
|
+
readonly kind: "durable";
|
|
75
93
|
/** 活体问答面;`undefined` ⇒ 恒 durable park 的原形门。 */
|
|
76
94
|
readonly question: LiveQuestionFace | undefined;
|
|
77
95
|
/** 每会话「本会话不再询问」探针(canonical toolName 键空间)。 */
|
|
@@ -79,6 +97,14 @@ export interface DurableApprovalSeat {
|
|
|
79
97
|
/** 豁免短路时的审计钩子。 */
|
|
80
98
|
readonly onExempted?: DurableAskOptions["onExempted"];
|
|
81
99
|
}
|
|
100
|
+
/** live 席:Runner 的活 onAsk 当场服 ask(无 park、无会话豁免店)。 */
|
|
101
|
+
export interface LiveApprovalSeat {
|
|
102
|
+
readonly kind: "live";
|
|
103
|
+
}
|
|
104
|
+
/** absent 席:这条腿没有谁服 ask ⇒ adjudicated allow-all,只服务零名单。 */
|
|
105
|
+
export interface AbsentApprovalSeat {
|
|
106
|
+
readonly kind: "absent";
|
|
107
|
+
}
|
|
82
108
|
/**
|
|
83
109
|
* [ref]([ref] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
|
|
84
110
|
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
@@ -179,8 +205,9 @@ export interface OperatorWarning {
|
|
|
179
205
|
}
|
|
180
206
|
/** {@link buildOnlySensitiveBaselineWarning} 读的座位量 —— 「这个部署到底有没有一道**真**门」。 */
|
|
181
207
|
export interface GateSeatView {
|
|
182
|
-
/**
|
|
183
|
-
|
|
208
|
+
/** 这条腿审批基线的**席**(与喂 {@link createApprovalBaselinePolicy} 的是同一只,S-694:此前是一个与席分家的
|
|
209
|
+
* `durableEnabled` 布尔,于是有活 onAsk 席的 run-local 腿被判成「没有门」)。 */
|
|
210
|
+
readonly approvalSeat: ApprovalSeatKind;
|
|
184
211
|
/** 单用户 turnkey 的 auto-accept 基线是否适用(= 零门意图的既定姿势,不是 misconfig)。 */
|
|
185
212
|
readonly singleUserAutoAcceptBaseline: boolean;
|
|
186
213
|
}
|
|
@@ -192,8 +219,10 @@ export interface GateSeatView {
|
|
|
192
219
|
* 此前是「这个部署一个门都没接」这个 misconfig 的**唯一**提示,而守卫集只挡那二十来个路径段、其余写
|
|
193
220
|
* 目标照旧无裁决 —— 信号不能因为我们铺了基线就静默消失,所以由我们自己按同一判据说一次。
|
|
194
221
|
*
|
|
195
|
-
* 判据(与信号消失的条件逐字互补):守卫集在场 ∧
|
|
196
|
-
* auto-accept
|
|
222
|
+
* 判据(与信号消失的条件逐字互补):守卫集在场 ∧ 这条腿的审批基线**没有席**(`absent`)∧ 单用户
|
|
223
|
+
* auto-accept 基线不适用。三个量都是部署常量 ⇒ 消费腿在 boot 期说一次,不是每任务一次。
|
|
224
|
+
* S-694:席读的是喂审批基线的**同一只**({@link ApprovalSeat})—— `durable` / `live` 两形都有人服 ask、名单
|
|
225
|
+
* 进策略,「守卫集是唯一的策略」这句话在它们上面不成立。今天真发的只剩 HTTP 腿「durable 关 ∧ 多租户」那一形。
|
|
197
226
|
*
|
|
198
227
|
* ⚠️ 单一属主([ref] 件三):HTTP 腿与 run-local 腿共用本判据与文案。两处各写一份 = 一处改了另一处
|
|
199
228
|
* 没改,而两份都长得像对的 —— 那正是本文件存在的理由。
|
|
@@ -211,18 +240,22 @@ export declare function createDeploymentGovernanceInputs(config: DeploymentGover
|
|
|
211
240
|
* 审批基线 —— `applyRuntimeGovernance` 那个 base 的 `toolPolicy` 座([ref] 件一)。**恒非
|
|
212
241
|
* `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
|
|
213
242
|
*
|
|
214
|
-
*
|
|
215
|
-
* · `durable`
|
|
216
|
-
*
|
|
217
|
-
* · `
|
|
243
|
+
* 三形,按席穷举(S-694,席的定义见 {@link ApprovalSeat}):
|
|
244
|
+
* · `durable` ⇒ AskUserQuestion 判决门 + 三张名单的审批门(deny/neverAuto/预算/会话豁免全在
|
|
245
|
+
* `createDurableAskPolicy` 里)。gated `ask` 由 core 变成 durable checkpoint suspend。
|
|
246
|
+
* · `live` ⇒ **同一只**三张名单审批门,不叠问答门。gated `ask` 由 core 交给 Runner 的活 onAsk 席
|
|
247
|
+
* (本口**不**做 ask→deny 映射:无人可问时的拒由那只席说,判据与文案都归它)。
|
|
248
|
+
* · `absent` ⇒ **adjudicated allow-all**(`createAllowDenyPolicy({})`):一条**在场的**、
|
|
218
249
|
* effect-aware 的策略。CC 的信任模型是 auto-accept,但门**机制**必须在场(core 原则「机制留、默认
|
|
219
250
|
* 可更宽」)—— 满足 core 的 `hasEffectAwareGate`,恢复可观测性与 hook/tighten 点,而运维照旧用
|
|
220
251
|
* `AUTONOMY`/`commandPolicy` 收紧不可逆操作(由 `applyRuntimeGovernance` tighten-only 叠上)。
|
|
252
|
+
* 🔴 本形**只**服务零名单:名单在场却递 absent 席 = 名单被静默吞掉(S-694 的病形),响亮拒。
|
|
221
253
|
*
|
|
222
254
|
* ⚠️ 「零门意图才铺 allow-all」这条准入判据**不在本口**:它是消费腿的部署形判断(单用户/多租户、
|
|
223
|
-
* 有无 checkpoint 店),属主是 `hasOperatorGateIntent
|
|
224
|
-
*
|
|
255
|
+
* 有无 checkpoint 店),属主是 `hasOperatorGateIntent` / `gateIntentUnserviceableReason`(src/approval.ts)。
|
|
256
|
+
* 本口只按调用方给的席铸策略,绝不替它判「这个部署该不该有门」;absent 臂那条拒是**席与名单自相矛盾**的
|
|
257
|
+
* 一致性断言(调用方已判过的事实在这里被违反),不是第二份准入判据。
|
|
225
258
|
*/
|
|
226
|
-
export declare function createApprovalBaselinePolicy(config: ApprovalBaselineConfigView,
|
|
259
|
+
export declare function createApprovalBaselinePolicy(config: ApprovalBaselineConfigView, seat: ApprovalSeat): ToolPolicy;
|
|
227
260
|
export {};
|
|
228
261
|
//# sourceMappingURL=deployment-governance.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, posix } from "node:path";
|
|
2
2
|
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, isAbsoluteForFamily, isProtectedWrite, ok, pathTargetValue, protectivePathTargetOf, } from "@sema-agent/core";
|
|
3
|
-
import { createDurableAskPolicy } from "./approval.js";
|
|
3
|
+
import { createDurableAskPolicy, hasApprovalListIntent } from "./approval.js";
|
|
4
4
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
|
|
5
5
|
export function createDurableQuestionGate(live) {
|
|
6
6
|
if (live === undefined)
|
|
@@ -58,8 +58,19 @@ export function assertGuardPatternsUsable(config) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
export function buildOnlySensitiveBaselineWarning(config, seat) {
|
|
61
|
-
if (config.sensitiveWritePatterns.length === 0 || seat.
|
|
61
|
+
if (config.sensitiveWritePatterns.length === 0 || seat.singleUserAutoAcceptBaseline)
|
|
62
62
|
return undefined;
|
|
63
|
+
switch (seat.approvalSeat) {
|
|
64
|
+
case "durable":
|
|
65
|
+
case "live":
|
|
66
|
+
return undefined;
|
|
67
|
+
case "absent":
|
|
68
|
+
break;
|
|
69
|
+
default: {
|
|
70
|
+
const unhandled = seat.approvalSeat;
|
|
71
|
+
throw new Error(`approval seat "${String(unhandled)}" has no arm in the only-sensitive-baseline warning`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
return {
|
|
64
75
|
event: "tool_policy_only_sensitive_baseline",
|
|
65
76
|
fields: {
|
|
@@ -95,16 +106,33 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
|
|
|
95
106
|
...(sensitivePathPolicy ? { sensitivePathPolicy } : {}),
|
|
96
107
|
};
|
|
97
108
|
}
|
|
98
|
-
export function createApprovalBaselinePolicy(config,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
export function createApprovalBaselinePolicy(config, seat) {
|
|
110
|
+
switch (seat.kind) {
|
|
111
|
+
case "durable":
|
|
112
|
+
return combinePolicies(createDurableQuestionGate(seat.question), createApprovalListPolicy(config, seat));
|
|
113
|
+
case "live":
|
|
114
|
+
return createApprovalListPolicy(config, {});
|
|
115
|
+
case "absent":
|
|
116
|
+
if (hasApprovalListIntent(config)) {
|
|
117
|
+
throw new Error("approval baseline: this leg has no approval seat (nobody can answer an ask), yet an approval list is in force " +
|
|
118
|
+
"(APPROVAL_REQUIRE / APPROVAL_DENY / APPROVAL_NEVER_AUTO, or the center's governance.approvalRequire) — the list would be silently ignored. " +
|
|
119
|
+
"The caller must hand a durable or live seat, or refuse the list before it reaches this leg.");
|
|
120
|
+
}
|
|
121
|
+
return createAllowDenyPolicy({});
|
|
122
|
+
default: {
|
|
123
|
+
const unhandled = seat;
|
|
124
|
+
throw new Error(`approval seat ${JSON.stringify(unhandled)} has no baseline arm`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function createApprovalListPolicy(config, memory) {
|
|
129
|
+
return createDurableAskPolicy({
|
|
102
130
|
requireApproval: config.approvalRequire,
|
|
103
131
|
deny: config.approvalDeny,
|
|
104
132
|
autoBudget: config.approvalAutoBudget,
|
|
105
133
|
neverAuto: config.approvalNeverAuto,
|
|
106
|
-
...(
|
|
107
|
-
...(
|
|
108
|
-
})
|
|
134
|
+
...(memory.exempt ? { exempt: memory.exempt } : {}),
|
|
135
|
+
...(memory.onExempted ? { onExempted: memory.onExempted } : {}),
|
|
136
|
+
});
|
|
109
137
|
}
|
|
110
138
|
//# sourceMappingURL=deployment-governance.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-699 / S-713 —— **名单型 env 旋钮**的闭集表(纯叶:零值依赖,任何层都可值引而不成装载环)。
|
|
3
|
+
*
|
|
4
|
+
* 解析规则(未设 / 形错拒启 / 姿态 / 生效名单)的全文在 `config.ts` `parseEnvNameAllowlist` 的头注;本表只装**各旋钮自己的**事实,
|
|
5
|
+
* 不是解析规则:
|
|
6
|
+
* · `example` 进拒句;
|
|
7
|
+
* · `multiTenant` = `REQUIRE_PRINCIPAL=true` 下名单作不作数。
|
|
8
|
+
* - `LSP_ENV_ALLOW` ⇒ ignored:host 腿语言服务器在多租户姿态下本来就不起,名单无处作数,设了要说出来(S-699)。
|
|
9
|
+
* - 两只沙箱名单 ⇒ honored:S-713 照录修前语义(任何姿态下都转发),S-715 主会话裁定维持 —— 沙箱 env 是部署方为隔离沙箱显式配的
|
|
10
|
+
* 带外凭据(e2b / docker 车道本来就是多租户 fleet 的主形),与 LSP 那条「本机子进程」的形不同;多租户下起服说一句。
|
|
11
|
+
* 表是 `Record<旋钮, …>`:闭集加一只旋钮而不补行 = 编译红。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **姿态列只在这里写一次**([ref];7.101.0 合并复审 O9):凡要对人说「多租户下哪只旋钮忽略、哪只照转发」的句子,一律取
|
|
14
|
+
* {@link describeAllowlistPostures} 的读数 —— 今天三处:`config_env_forwarded_multi_tenant.note`(`config.ts` 解析器)、LSP 剥除现场的
|
|
15
|
+
* 出路句(`observability/secret-env-scrub.ts`)、配置目录 `LSP_ENV_ALLOW` 行(`config-catalog.ts`)。修前三处各手抄一遍,表改一格它们
|
|
16
|
+
* 当场撒谎。为什么是纯叶:`config-catalog.ts` 被 `config.ts` 值引,表留在 `config.ts` 会成装载环。
|
|
17
|
+
*/
|
|
18
|
+
export type EnvNameAllowlistKnob = "LSP_ENV_ALLOW" | "E2B_SANDBOX_ENV" | "DOCKER_SANDBOX_ENV";
|
|
19
|
+
export type EnvNameAllowlistPosture = {
|
|
20
|
+
readonly kind: "honored";
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "ignored";
|
|
23
|
+
readonly why: string;
|
|
24
|
+
};
|
|
25
|
+
export type EnvNameAllowlistKnobTable = {
|
|
26
|
+
readonly [K in EnvNameAllowlistKnob]: {
|
|
27
|
+
readonly example: string;
|
|
28
|
+
readonly multiTenant: EnvNameAllowlistPosture;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare const ENV_NAME_ALLOWLIST_KNOBS: EnvNameAllowlistKnobTable;
|
|
32
|
+
/**
|
|
33
|
+
* 多租户姿态句 —— **由表派生**,不许在别处手写。形:`under REQUIRE_PRINCIPAL=true: <旋钮> is ignored; <旋钮> / <旋钮> are forwarded as configured`
|
|
34
|
+
* (按 {@link POSTURE_PHRASE} 的 kind 序分组、组内按表序;空组不出现)。只有旋钮名与姿态,不带任何名单内容。
|
|
35
|
+
*/
|
|
36
|
+
export declare function describeAllowlistPostures(table?: EnvNameAllowlistKnobTable): string;
|
|
37
|
+
//# sourceMappingURL=env-name-allowlist-knobs.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const ENV_NAME_ALLOWLIST_KNOBS = {
|
|
2
|
+
LSP_ENV_ALLOW: {
|
|
3
|
+
example: "LSP_ENV_ALLOW=NPM_TOKEN,CARGO_REGISTRIES_MY_REG_TOKEN",
|
|
4
|
+
multiTenant: { kind: "ignored", why: "the host-lane language server is not started in this posture, and the language-server env keeps the full secret strip" },
|
|
5
|
+
},
|
|
6
|
+
E2B_SANDBOX_ENV: { example: "E2B_SANDBOX_ENV=GIT_TOKEN,NPM_TOKEN", multiTenant: { kind: "honored" } },
|
|
7
|
+
DOCKER_SANDBOX_ENV: { example: "DOCKER_SANDBOX_ENV=GIT_TOKEN,NPM_TOKEN", multiTenant: { kind: "honored" } },
|
|
8
|
+
};
|
|
9
|
+
const POSTURE_PHRASE = {
|
|
10
|
+
ignored: { one: "is ignored", many: "are ignored" },
|
|
11
|
+
honored: { one: "is forwarded as configured", many: "are forwarded as configured" },
|
|
12
|
+
};
|
|
13
|
+
export function describeAllowlistPostures(table = ENV_NAME_ALLOWLIST_KNOBS) {
|
|
14
|
+
const knobs = Object.keys(table);
|
|
15
|
+
const groups = Object.keys(POSTURE_PHRASE).flatMap((kind) => {
|
|
16
|
+
const named = knobs.filter((k) => table[k].multiTenant.kind === kind);
|
|
17
|
+
if (named.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const phrase = POSTURE_PHRASE[kind];
|
|
20
|
+
return [`${named.join(" / ")} ${named.length === 1 ? phrase.one : phrase.many}`];
|
|
21
|
+
});
|
|
22
|
+
return `under REQUIRE_PRINCIPAL=true: ${groups.join("; ")}`;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=env-name-allowlist-knobs.js.map
|
|
@@ -248,13 +248,19 @@ export type FleetFrame = {
|
|
|
248
248
|
* durable 行压根不存在 —— 三形共同点是**没有终局可投影**,而本副本的行必然陈旧。此时造一个终态
|
|
249
249
|
* 就是编:判死的属主始终是 durable 侧的 reaper。所以只陈述「这条行离场了」,不逼消费端结算。
|
|
250
250
|
* (cli 侧裁量 [ref]:渲「离场」**不清行** —— 行保留、中性离场形、active 扣除、折叠排后。)
|
|
251
|
+
* 🔴 S-751:子代行的陈述级退场之前,对账腿先问过它的属主(bg registry 非终态 ∨ 有待决审批 ⇒ 留行);
|
|
252
|
+
* 本帧因此只在「静默满一周期 ∧ 属主不作保」时出现。而它**不封被退那一代**(见 {@link cycleSeq})——
|
|
253
|
+
* 之后同一代(或更大代)的帧到达 = 那一行其实还活着,行**复活**;更小的代照旧是前代迟到帧。
|
|
251
254
|
* ADDITIVE / tolerate-absent。
|
|
252
255
|
*/
|
|
253
256
|
removeReason?: "orphaned";
|
|
254
257
|
/** [ref] §2 判死×判代合成规则:退场帧带**被退那一代**的 {@link FleetTaskRow.cycleSeq}。
|
|
255
258
|
* 消费端因此能把 `retire(gen=N)` 之后的 `frame(gen=N+1)` 读成**复活**(行重生,退休不粘住新代),
|
|
256
|
-
*
|
|
257
|
-
*
|
|
259
|
+
* 而**结算级** `retire(gen=N)`(本帧无 `removeReason`)之后的 `frame(gen=N)` 读成**前代迟到帧,忽略**。
|
|
260
|
+
* 🔴 S-751:**陈述级**退场(`removeReason:"orphaned"`)之后的 `frame(gen=N)` = **复活**,不是迟到帧 ——
|
|
261
|
+
* 陈述级退场从不断言「这一代结束了」(它说的只是「本副本一个周期没听到这一行」),所以同代的下一帧
|
|
262
|
+
* 就是它还活着的证据(本仓 {@link FleetEventBus.removeTask} 对这一形不封代,消费端同读);更小的代照旧忽略。
|
|
263
|
+
* 缺席同 {@link FleetTaskRow.cycleSeq} ——「无此概念」,不是「第一代」。 */
|
|
258
264
|
cycleSeq?: number;
|
|
259
265
|
} | {
|
|
260
266
|
type: "workflow";
|
|
@@ -452,7 +458,10 @@ export declare class FleetEventBus {
|
|
|
452
458
|
private readonly tasks;
|
|
453
459
|
private readonly workflows;
|
|
454
460
|
/** [ref] / codex R1-F2:**代际墓碑** —— 已离场行的最后已知 `cycleSeq`(行本身已从 `tasks` 删掉,
|
|
455
|
-
* 代际状态却必须活得比行久一点,否则前代迟到帧会在空 Map 上把幽灵行重铸出来)。有界 LRU。
|
|
461
|
+
* 代际状态却必须活得比行久一点,否则前代迟到帧会在空 Map 上把幽灵行重铸出来)。有界 LRU。
|
|
462
|
+
* 🔴 S-751:碑上多记一位 `ended` = 「**这一代结束了**」是否被断言过。离场**恒**保住代际状态(`gen` 之前的帧
|
|
463
|
+
* 永远是前代迟到帧 —— 与行还在 Map 里时那道闸同一个判据);只有作「这一代结束了」断言的离场(结算级 / 投影级
|
|
464
|
+
* 常规清场)才把 `gen` **这一代本身**也封掉。陈述级退场(`removeReason:"orphaned"`)不作这个断言 ⇒ 同代帧 = 复活。 */
|
|
456
465
|
private readonly retiredGen;
|
|
457
466
|
private static readonly RETIRED_GEN_MAX;
|
|
458
467
|
constructor(now?: () => number);
|
|
@@ -472,7 +481,7 @@ export declare class FleetEventBus {
|
|
|
472
481
|
/** Drop a task row (terminal + swept) + fan out the removal. Idempotent (a no-op if already gone).
|
|
473
482
|
*
|
|
474
483
|
* `opts`([ref],两键均 ADDITIVE):`removeReason:"orphaned"` = **陈述级退场**(没有结算语义,见
|
|
475
|
-
* {@link FleetFrame} 的 `task_remove`
|
|
484
|
+
* {@link FleetFrame} 的 `task_remove` 臂;S-751 起**不封被退那一代**,同代帧 = 复活);`cycleSeq` = 被退那一代的代际号。发布方的常规清场
|
|
476
485
|
* (`onTerminal`/`onChildTerminal`)不传 opts ⇒ 帧逐字节与本改动之前相同。 */
|
|
477
486
|
removeTask(id: string, opts?: {
|
|
478
487
|
removeReason?: "orphaned";
|
|
@@ -489,6 +498,23 @@ export declare class FleetEventBus {
|
|
|
489
498
|
backgroundChildLaneRow(scope: string | undefined, childUuid: string): string | undefined;
|
|
490
499
|
/** [ref] 让位迁移料的读面:迁移「只补缺席位」需要看得见存活行现有什么(BCE 是权威车道,不覆盖)。 */
|
|
491
500
|
taskRow(id: string): Readonly<FleetTaskRow> | undefined;
|
|
501
|
+
/**
|
|
502
|
+
* S-751:一条子代行在它两只**属主账**上的键。三个 id 域互不相同,换算只有本类做得了(两种行 id 都是本文件
|
|
503
|
+
* 铸的,两域之间唯一的 join —— 让位登记簿 —— 也住在这里),所以换算只写这一处:
|
|
504
|
+
* · 行 id —— BCE 车道 = registry handle 本身(`e.taskId`);run-leg 车道 = `${runId} ${子代 task_progress taskId}`
|
|
505
|
+
* (`fleetRunPublisher` 的 `childId`);
|
|
506
|
+
* · `handle` —— core bg registry / `BackgroundAgentStore` 的行键,**只认** `DURABLE_AGENT_HANDLE_RE` 形
|
|
507
|
+
* (`wa*` 不在 bg 账上);
|
|
508
|
+
* · `sessionIds` —— 子代自己的 core session id(uuid 域)= 待决审批 `originTaskId` 的值域
|
|
509
|
+
* (core `AskRequest.sourceTaskId`:「the issuing task's session id」),**不是** handle。是**集合**:同一行名下
|
|
510
|
+
* 可以先后挂过几个子代 session(`wa*` 重试换 session、复活换 session),让位登记簿对每一个都留着 claim ——
|
|
511
|
+
* 只取其一(此前取最早那个)会漏掉当前这一次的卡(codex r1 F3)。
|
|
512
|
+
* 换不出的键就缺席 / 空(不猜):父链接行 / 顶层 run 行两者皆无;BCE 行在本车道学到子代 uuid 之前 `sessionIds` 为空。
|
|
513
|
+
*/
|
|
514
|
+
childOwnerKeys(row: Pick<FleetTaskRow, "id" | "scope">): {
|
|
515
|
+
handle?: string;
|
|
516
|
+
sessionIds: readonly string[];
|
|
517
|
+
};
|
|
492
518
|
/** Upsert a workflow row (MERGE by `id`) + fan out. */
|
|
493
519
|
publishWorkflow(delta: Partial<FleetWorkflowRow> & {
|
|
494
520
|
id: string;
|
package/dist/fleet/fleet-bus.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { deriveAgentDisplayStatus, isDelegatedAgentTerminal } from "@sema-agent/core";
|
|
2
|
+
import { DURABLE_AGENT_HANDLE_RE, deriveAgentDisplayStatus, isDelegatedAgentTerminal } from "@sema-agent/core";
|
|
3
3
|
import { persistedPlaneOf } from "../terminal.js";
|
|
4
4
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
5
5
|
import { redactSecrets } from "../trace/redact.js";
|
|
@@ -74,7 +74,7 @@ export class FleetEventBus {
|
|
|
74
74
|
return;
|
|
75
75
|
if (existing === undefined && delta.retiredBy === undefined) {
|
|
76
76
|
const tomb = this.retiredGen.get(delta.id);
|
|
77
|
-
if (tomb !== undefined && delta.cycleSeq !== undefined && delta.cycleSeq
|
|
77
|
+
if (tomb !== undefined && delta.cycleSeq !== undefined && (delta.cycleSeq < tomb.gen || (tomb.ended && delta.cycleSeq === tomb.gen)))
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
const merged = { ...(existing ?? { id: delta.id, status: "running" }), ...delta };
|
|
@@ -97,10 +97,12 @@ export class FleetEventBus {
|
|
|
97
97
|
if (existing !== undefined && opts?.cycleSeq !== undefined && existing.cycleSeq !== undefined && existing.cycleSeq > opts.cycleSeq)
|
|
98
98
|
return;
|
|
99
99
|
if (this.tasks.delete(id)) {
|
|
100
|
-
const
|
|
100
|
+
const prior = this.retiredGen.get(id);
|
|
101
|
+
const gen = existing?.cycleSeq ?? opts?.cycleSeq ?? prior?.gen;
|
|
101
102
|
if (gen !== undefined) {
|
|
103
|
+
const ended = opts?.removeReason !== "orphaned" || (prior !== undefined && prior.gen === gen && prior.ended);
|
|
102
104
|
this.retiredGen.delete(id);
|
|
103
|
-
this.retiredGen.set(id, gen);
|
|
105
|
+
this.retiredGen.set(id, { gen, ended });
|
|
104
106
|
if (this.retiredGen.size > FleetEventBus.RETIRED_GEN_MAX) {
|
|
105
107
|
const oldest = this.retiredGen.keys().next();
|
|
106
108
|
if (!oldest.done)
|
|
@@ -138,6 +140,20 @@ export class FleetEventBus {
|
|
|
138
140
|
taskRow(id) {
|
|
139
141
|
return this.tasks.get(id);
|
|
140
142
|
}
|
|
143
|
+
childOwnerKeys(row) {
|
|
144
|
+
const sp = row.id.lastIndexOf(" ");
|
|
145
|
+
if (sp > 0) {
|
|
146
|
+
const sessionId = row.id.slice(sp + 1);
|
|
147
|
+
const claimed = this.bceClaims.get(FleetEventBus.bceKey(row.scope, sessionId));
|
|
148
|
+
return { sessionIds: [sessionId], ...(claimed !== undefined && DURABLE_AGENT_HANDLE_RE.test(claimed) ? { handle: claimed } : {}) };
|
|
149
|
+
}
|
|
150
|
+
const prefix = FleetEventBus.bceKey(row.scope, "");
|
|
151
|
+
const sessionIds = [];
|
|
152
|
+
for (const [k, rowId] of this.bceClaims)
|
|
153
|
+
if (rowId === row.id && k.startsWith(prefix))
|
|
154
|
+
sessionIds.push(k.slice(prefix.length));
|
|
155
|
+
return { sessionIds, ...(DURABLE_AGENT_HANDLE_RE.test(row.id) ? { handle: row.id } : {}) };
|
|
156
|
+
}
|
|
141
157
|
publishWorkflow(delta) {
|
|
142
158
|
const merged = { ...(this.workflows.get(delta.id) ?? { id: delta.id, status: "running" }), ...delta };
|
|
143
159
|
this.workflows.set(delta.id, merged);
|
|
@@ -33,10 +33,17 @@
|
|
|
33
33
|
* 「**静默满一个对账周期**」(本腿订阅总线自记每行的最后帧时刻):还在出帧 = 活着的证据。
|
|
34
34
|
* durable 行**在场**的那三格没有这个歧义(`getRun` 命中 = 它就是一条 run 行),故照矩阵即时处置。
|
|
35
35
|
*
|
|
36
|
-
* ## 子代行(§1)
|
|
37
|
-
*
|
|
38
|
-
* bg 子代的 durable 真源在 core 侧 registry,server
|
|
39
|
-
*
|
|
36
|
+
* ## 子代行(§1;🔴 S-751 起改为「活性由属主说了算」)
|
|
37
|
+
* 子代行**不读 durable run 店**(它不是 run 行)。它的活性属主有两只,都在本仓可读 —— 读它们**不是**越界重判,
|
|
38
|
+
* 是问属主(S-751 之前此处写「bg 子代的 durable 真源在 core 侧 registry,server 不越界重判」,于是判据里只剩
|
|
39
|
+
* 「静默」这一条,而 registry 的读半场 `RunnerDeps.backgroundAgentStore` 一直就在本仓):
|
|
40
|
+
* ① core bg registry(`BackgroundAgentStore` 行 `status` 非终态 = `running` / `parked`);
|
|
41
|
+
* ② 本副本审批面的待决卡(子代发起、没人答的 ask —— 子代卡在那儿,当然不出帧)。
|
|
42
|
+
* **一条规则**:无父可循(父从未在 / 父本周期刚退休或退场)∧ 静默满一个对账周期 ∧ 两只属主都不作保 ⇒
|
|
43
|
+
* 陈述级退场。父离场**不再无条件同扫**子代(那是报案的一半病:父轮结束、子代一次工具调用 ≥60s 或审批卡
|
|
44
|
+
* 60s 没人答 ⇒ 活子代被扫);父离场只让子代「无父可循」,子代与既有孤儿走**同一只**判据、同一个循环。
|
|
45
|
+
* 同一问也挂在「durable 无行」那一格上:父链接未解析的 `a*` 行与顶层行同形(见上方收窄段),run 账不认识它
|
|
46
|
+
* ≠ 它死了。另一半病(陈述级退场立了代际墓碑、吞掉活子代之后的同代帧)的修在 `fleet-bus.ts` `removeTask`。
|
|
40
47
|
*
|
|
41
48
|
* ## workflow 行(§1)
|
|
42
49
|
* 只做同款「durable 终局投影」([ref] 的 snapshot pull 腿 + reapers 的 workflow recover 腿已在场),
|
|
@@ -77,7 +84,7 @@
|
|
|
77
84
|
* 与 `fleet-terminal-window.ts` 同一论证:两侧边界都由语义定死(比周期短 = 幽灵行仍在面板上,比周期长 =
|
|
78
85
|
* 拿对账腿当历史列表),旋钮只会让部署方去调一个没有正确取值的数;fleet 面现有零旋钮,从之。
|
|
79
86
|
*/
|
|
80
|
-
import { type WorkflowRun, type WorkflowRunStore } from "@sema-agent/core";
|
|
87
|
+
import { type BackgroundAgentRecord, type WorkflowRun, type WorkflowRunStore } from "@sema-agent/core";
|
|
81
88
|
import type { Logger } from "../observability/logger.js";
|
|
82
89
|
import type { Metrics } from "../observability/metrics.js";
|
|
83
90
|
import { type RunRecord } from "../plugins/store-contracts.js";
|
|
@@ -111,6 +118,12 @@ export interface FleetReconcileRunPort {
|
|
|
111
118
|
export interface FleetReconcileWorkflowPort {
|
|
112
119
|
get(id: string): Promise<WorkflowRun | null | undefined>;
|
|
113
120
|
}
|
|
121
|
+
/** S-751:core bg registry 账(`BackgroundAgentStore`)的只读窄口 —— 与 {@link FleetReconcileRunPort} 同一条
|
|
122
|
+
* 单写者纪律(接口上没有写方法)。生产 = `RunnerDeps.backgroundAgentStore` **同实例**(结构上就是它的 `get`
|
|
123
|
+
* 一面);`null` = 账上没有这一行。 */
|
|
124
|
+
export interface FleetReconcileAgentPort {
|
|
125
|
+
get(handle: string, scope: string): Promise<Pick<BackgroundAgentRecord, "status"> | null>;
|
|
126
|
+
}
|
|
114
127
|
export interface FleetReconcilerDeps {
|
|
115
128
|
bus: FleetEventBus;
|
|
116
129
|
/**
|
|
@@ -130,6 +143,14 @@ export interface FleetReconcilerDeps {
|
|
|
130
143
|
subscribeRunTerminal?: (fn: (rec: RunTerminalRecord) => void) => () => void;
|
|
131
144
|
runs?: FleetReconcileRunPort;
|
|
132
145
|
workflows?: FleetReconcileWorkflowPort | WorkflowRunStore;
|
|
146
|
+
/** S-751:子代活性的属主①(bg registry 账)。缺席 = 没有 bg 店的部署(纯内存 dev),这一问问不了。 */
|
|
147
|
+
agents?: FleetReconcileAgentPort;
|
|
148
|
+
/**
|
|
149
|
+
* S-751:子代活性的属主②:这只子代此刻有没有**待决**审批卡。只问是非(与 {@link isLiveOnThisReplica} 同姿势,
|
|
150
|
+
* 不搬 Map)。入参是 ask 发起者域(子代自己的 core session id,= 卡上的 `originTaskId`),**不是**行 id ——
|
|
151
|
+
* 行 id → 该域的换算只在 `FleetEventBus.childOwnerKeys`。生产 = `ToolApprovalCoordinator.hasLivePendingFrom`。
|
|
152
|
+
*/
|
|
153
|
+
hasPendingAsk?: (originTaskId: string) => boolean;
|
|
133
154
|
/** 本副本 id(与 `RunRecord.instanceId` 同域)。只进日志/读数 —— 两条 `running` 臂的**处置相同**
|
|
134
155
|
* (都是陈述级退场),分不分得出 claim 属谁不改变行为,故它不是判据。 */
|
|
135
156
|
instanceId?: string;
|
|
@@ -150,8 +171,10 @@ export interface FleetReconcileTally {
|
|
|
150
171
|
retired: number;
|
|
151
172
|
/** 陈述级退场的行数(顶层 + 孤儿子行)。 */
|
|
152
173
|
orphaned: number;
|
|
153
|
-
/**
|
|
174
|
+
/** 父本周期刚离场、自己又判成孤儿而陈述级退场的子行数(S-751 前 = 随父无条件同扫的数)。 */
|
|
154
175
|
childrenSwept: number;
|
|
176
|
+
/** S-751:静默满一周期、无父可循(或 run 账不认识),但属主作保而**留行**的子行数。 */
|
|
177
|
+
childrenKeptAlive: number;
|
|
155
178
|
/** durable 终局 ⇒ 投影退休的 workflow 行数。 */
|
|
156
179
|
workflowsRetired: number;
|
|
157
180
|
/** 本周期实际发出的 durable 点读次数。 */
|