@sema-agent/server 7.100.0 → 7.101.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 +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/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-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 +9 -1
- package/dist/observability/fail-open.js +9 -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 +65 -31
- package/dist/tool-approval.js +96 -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
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* - **可选分层(D3)**:仅当 center 已配置(sema-registry lane 的 baseUrl+token+worker)∧ `FLEET_ADVERTISE_ADDRESS`
|
|
5
5
|
* 显式声明时才启动 —— 不配 center 的部署零行为变化。address 不自猜(k8s pod IP / docker alias / 127.0.0.1:port
|
|
6
6
|
* 只有部署环境自己知道,故用显式 env)。
|
|
7
|
-
* - **announce=注册=心跳同形状幂等 upsert**:boot 后 POST 一次 + 每 heartbeatMs 续;`ready =
|
|
7
|
+
* - **announce=注册=心跳同形状幂等 upsert**:boot 后 POST 一次 + 每 heartbeatMs 续;`ready` 今 = `!draining`(实现只读
|
|
8
|
+
* `isDraining()`,announce 本身在 boot 之后才起;S-740 起与 /health 就绪同一只谓词 —— 车OC 6b 注释改真,实现归 S-740)。
|
|
8
9
|
* - **draining 翻转即刻再 announce**(第一时间自摘流,比任何观测都快);退出前 DELETE(显式注销,幂等)。
|
|
9
10
|
* - **usage 批报**:tracer 的 `brain.call` 已带 ALS principal —— FleetUsageAccumulator 按 principal×model
|
|
10
11
|
* 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
|
|
2
|
-
import { matchCatalogModel } from "../model-select.js";
|
|
2
|
+
import { matchCatalogModel, refusedCatalogEntry } from "../model-select.js";
|
|
3
|
+
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
3
4
|
import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
|
|
4
5
|
import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
|
|
5
6
|
export function buildHookFailureText(raw) {
|
|
@@ -32,21 +33,29 @@ export function createHookLlm(deps) {
|
|
|
32
33
|
};
|
|
33
34
|
const hookModelFor = (requested) => {
|
|
34
35
|
const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
|
|
36
|
+
const unavailable = (field, ref) => {
|
|
37
|
+
if (config.models[ref] !== undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const hit = refusedCatalogEntry(ref, config.modelRefusals);
|
|
40
|
+
return hit === undefined ? undefined : { ok: false, error: modelUnavailableMessage(field, ref, hit.name, hit.refusal) };
|
|
41
|
+
};
|
|
35
42
|
if (requested === undefined) {
|
|
36
43
|
const sum = config.roles.summarize;
|
|
37
44
|
const roleModel = typeof sum === "string" ? sum : sum && typeof sum === "object" && "model" in sum && typeof sum.model === "string" ? sum.model : undefined;
|
|
38
45
|
if (roleModel !== undefined)
|
|
39
|
-
return config.models[roleModel] ? shape(roleModel) : shapeFrom(config.model.id, config.model, roleModel);
|
|
46
|
+
return unavailable("summarize role model", roleModel) ?? (config.models[roleModel] ? shape(roleModel) : shapeFrom(config.model.id, config.model, roleModel));
|
|
40
47
|
return shapeFrom(config.model.id, config.model, config.model.id);
|
|
41
48
|
}
|
|
42
49
|
if (config.models[requested])
|
|
43
50
|
return shape(requested);
|
|
44
|
-
|
|
51
|
+
if (Object.hasOwn(config.modelRefusals ?? {}, requested))
|
|
52
|
+
return unavailable("hook model", requested);
|
|
53
|
+
const byId = matchCatalogModel(requested, config.models, config.modelRefusals);
|
|
45
54
|
if (byId !== undefined)
|
|
46
55
|
return shape(byId);
|
|
47
56
|
if (requested === config.model.id)
|
|
48
57
|
return shapeFrom(requested, config.model, requested);
|
|
49
|
-
return { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
|
|
58
|
+
return unavailable("hook model", requested) ?? { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
|
|
50
59
|
};
|
|
51
60
|
const judgeRoute = (pick, perModelKey, deploymentKey, declaredRoot) => {
|
|
52
61
|
const credential = resolveRouteCredential({
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* `scrubServerSecrets` 的结果)。
|
|
10
10
|
*
|
|
11
11
|
* ## 规则(一句)
|
|
12
|
-
* 语言服务器子进程的 env = {@link scrubServerSecrets}(process.env)
|
|
12
|
+
* 语言服务器子进程的 env = {@link scrubServerSecrets}(process.env) ∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
|
|
13
|
+
* 剥除与模型可驱动的 host shell(`inheritEnv:"scrub"`)
|
|
13
14
|
* 和 project-memory 的 git 子进程**同一只函数**:目录 `type:"secret"` 键按名剥 ∘ core 形状规则按形剥。只交 core 的默认
|
|
14
15
|
* 会漏掉形状规则认不出的本仓凭据键(今天是 `OTEL_EXPORTER_OTLP_HEADERS`),那正是 S-684 当初的病形。
|
|
15
16
|
* 剥掉了什么经既有观测面留痕(site = `server.lsp.spawn-env`,每 site×key 一次披露 + 逐次计数)—— core 那侧
|
|
@@ -19,11 +20,22 @@
|
|
|
19
20
|
* env 在**构造时**取一次快照(boot / run-local 起腿时),而不是 core 默认那样每次 spawn 现取:`NodeLspManagerOptions.env`
|
|
20
21
|
* 是一份静态记录。本仓的 `process.env` 是 boot 期输入(热配置走配置中心,不改 `process.env`),两者在本仓部署上同值。
|
|
21
22
|
*
|
|
23
|
+
* ## 点名放行(S-699,`LSP_ENV_ALLOW`)
|
|
24
|
+
* 剥除会连语言服务器自己的真实依赖一起剥掉(`.npmrc` 的 `${NPM_TOKEN}` 插值、rust-analyzer `cargo metadata` 私有 registry 的
|
|
25
|
+
* `CARGO_REGISTRIES_<名>_TOKEN` —— 形状规则按名判,分不出「给语言服务器用的令牌」与「编排器的令牌」)。出路 = 部署方点名:
|
|
26
|
+
* env = {@link scrubServerSecrets}(process.env) 之上把 `config.lspEnvAllow` 点到的键经 {@link allowlistedEnv}(与 host shell
|
|
27
|
+
* `inheritEnv:[names]` 同一只谓词)从原 env 原样放回;没点到的照旧剥。名单由 `config.ts` 解析(形错拒启;
|
|
28
|
+
* `REQUIRE_PRINCIPAL=true` 下落空名单并 warn 一次),本函数只读 `config.lspEnvAllow`,不在这里再判一次姿态。
|
|
29
|
+
* 放回的键不是剥除 ⇒ 不进 `secret_env_scrubbed` 的键表与计数;剥掉的键经既有现场 `server.lsp.spawn-env` 一行 warn
|
|
30
|
+
* (带当前放行名单与出路句),这一行就是「剥了什么」的用户可见面 —— 语言服务器侧从不报。
|
|
31
|
+
*
|
|
22
32
|
* ## 两个调用点
|
|
23
33
|
* `boot/execution-env.ts`(服务的 host 车道)与 `run-local.ts`(纯本地 CLI)此前各写一遍 `new NodeLspManager({ log })`;
|
|
24
34
|
* 现在都经本函数 —— 构造形只有一份,剥除规则也就只能有一份(源码对账钉在 `test/secret-env-scrub-disclosure.test.ts`)。
|
|
35
|
+
* 两处都交整份 `config`(类型只要 `lspEnvAllow` 一格),放行名单因此只有一个读点。
|
|
25
36
|
*/
|
|
26
37
|
import { NodeLspManager } from "@sema-agent/core";
|
|
38
|
+
import type { ServiceConfig } from "./config-types.js";
|
|
27
39
|
/** 构造宿主车道的本地语言服务器管理器(`create*`:返回活对象)。`log` = 管理器的观测钩(open/heal/degrade/evict 事件)。 */
|
|
28
|
-
export declare function createHostLspManager(log: (event: string, fields: Record<string, unknown>) => void): NodeLspManager;
|
|
40
|
+
export declare function createHostLspManager(config: Pick<ServiceConfig, "lspEnvAllow">, log: (event: string, fields: Record<string, unknown>) => void): NodeLspManager;
|
|
29
41
|
//# sourceMappingURL=host-lsp-manager.d.ts.map
|
package/dist/host-lsp-manager.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { NodeLspManager } from "@sema-agent/core";
|
|
2
2
|
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
3
|
-
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
4
|
-
export function createHostLspManager(log) {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
3
|
+
import { allowlistedEnv, scrubServerSecrets } from "./server-secret-env.js";
|
|
4
|
+
export function createHostLspManager(config, log) {
|
|
5
|
+
const scrubFindings = [];
|
|
6
|
+
const composed = { ...scrubServerSecrets(process.env, scrubFindings), ...allowlistedEnv(config.lspEnvAllow, process.env) };
|
|
7
|
+
const findings = scrubFindings.filter((f) => composed[f.key] === undefined);
|
|
8
|
+
recordSecretEnvScrub("server.lsp.spawn-env", findings, config.lspEnvAllow);
|
|
8
9
|
const env = {};
|
|
9
|
-
for (const [k, v] of Object.entries(
|
|
10
|
+
for (const [k, v] of Object.entries(composed))
|
|
10
11
|
if (v !== undefined)
|
|
11
12
|
env[k] = v;
|
|
12
13
|
return new NodeLspManager({ log, env });
|
package/dist/http/admission.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isThinkingLevel, expandTiers } from "@sema-agent/core";
|
|
2
|
-
import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
|
|
2
|
+
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
|
|
3
|
+
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
3
4
|
import { HttpError } from "../security.js";
|
|
4
5
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
6
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
@@ -39,7 +40,15 @@ const MAX_IMAGES_PER_REQUEST = 20;
|
|
|
39
40
|
const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
|
|
40
41
|
function mentionableNames(config, catalog) {
|
|
41
42
|
const allowlist = config.atModelAllowlist ?? [];
|
|
42
|
-
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
|
|
43
|
+
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist, config.modelRefusals));
|
|
44
|
+
}
|
|
45
|
+
function refuseUnavailableModel(res, config, field, ref) {
|
|
46
|
+
const hit = refusedCatalogEntry(ref, config.modelRefusals);
|
|
47
|
+
if (hit === undefined)
|
|
48
|
+
return false;
|
|
49
|
+
const message = modelUnavailableMessage(field, ref.slice(0, 120), hit.name, hit.refusal);
|
|
50
|
+
sendError(res, 400, "request.model_unavailable", message);
|
|
51
|
+
return true;
|
|
43
52
|
}
|
|
44
53
|
export function createAdmission(ctx) {
|
|
45
54
|
const { deps, readJson, runnerFor } = ctx;
|
|
@@ -419,8 +428,10 @@ export function createAdmission(ctx) {
|
|
|
419
428
|
return null;
|
|
420
429
|
}
|
|
421
430
|
const agentsBare = deps.config.models ?? {};
|
|
422
|
-
const agentsCatalog =
|
|
431
|
+
const agentsCatalog = selectableCatalog(agentsBare, deps.config.tiers, deps.config.modelRefusals);
|
|
423
432
|
const um = unknownTaskAgentModelRef(body.agents, agentsCatalog);
|
|
433
|
+
if (um !== undefined && refuseUnavailableModel(res, deps.config, `agents[${um.index}].model`, um.ref))
|
|
434
|
+
return null;
|
|
424
435
|
if (um !== undefined) {
|
|
425
436
|
sendError(res, 400, "request.unknown_reference", `unknown agents[${um.index}].model "${um.ref.slice(0, 120)}" — not in the configured catalog (a catalog name or tier word)`, { available: mentionableNames(deps.config, agentsCatalog) });
|
|
426
437
|
return null;
|
|
@@ -494,8 +505,10 @@ export function createAdmission(ctx) {
|
|
|
494
505
|
return null;
|
|
495
506
|
}
|
|
496
507
|
const bare = deps.config.models ?? {};
|
|
497
|
-
const catalog =
|
|
498
|
-
if (matchCatalogModel(body.model, catalog) === undefined) {
|
|
508
|
+
const catalog = selectableCatalog(bare, deps.config.tiers, deps.config.modelRefusals);
|
|
509
|
+
if (matchCatalogModel(body.model, catalog, deps.config.modelRefusals) === undefined) {
|
|
510
|
+
if (refuseUnavailableModel(res, deps.config, "model", body.model))
|
|
511
|
+
return null;
|
|
499
512
|
sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
500
513
|
return null;
|
|
501
514
|
}
|
|
@@ -506,8 +519,10 @@ export function createAdmission(ctx) {
|
|
|
506
519
|
return null;
|
|
507
520
|
}
|
|
508
521
|
const bare = deps.config.models ?? {};
|
|
509
|
-
const catalog =
|
|
510
|
-
if (matchCatalogModel(body.compactionModel, catalog) === undefined) {
|
|
522
|
+
const catalog = selectableCatalog(bare, deps.config.tiers, deps.config.modelRefusals);
|
|
523
|
+
if (matchCatalogModel(body.compactionModel, catalog, deps.config.modelRefusals) === undefined) {
|
|
524
|
+
if (refuseUnavailableModel(res, deps.config, "compactionModel", body.compactionModel))
|
|
525
|
+
return null;
|
|
511
526
|
sendError(res, 400, "request.unknown_reference", `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
512
527
|
return null;
|
|
513
528
|
}
|