@sema-agent/server 3.0.0 → 3.2.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +13 -9
- package/dist/approval-hmac.js +1 -2
- package/dist/bench/s1/live-deps.js +1 -1
- package/dist/boot/config-center.d.ts +3 -2
- package/dist/boot/config-center.js +33 -19
- package/dist/boot/resolve-spec.js +5 -5
- package/dist/brain.js +42 -9
- package/dist/config-center/apply-effective.d.ts +1 -1
- package/dist/config-center/apply-effective.js +4 -4
- package/dist/config-center/facade.d.ts +47 -0
- package/dist/config-center/facade.js +46 -0
- package/dist/config-center/http-client.d.ts +1 -1
- package/dist/config-center/http-client.js +6 -6
- package/dist/config-center/restart-signal.d.ts +11 -3
- package/dist/config-center/restart-signal.js +51 -7
- package/dist/config-center/skills-mcp.js +1 -1
- package/dist/config-center/types.d.ts +2 -2
- package/dist/config-provider.d.ts +7 -7
- package/dist/config-provider.js +9 -9
- package/dist/config.d.ts +1 -1
- package/dist/config.js +5 -5
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/runs.js +7 -7
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +3 -3
- package/dist/key-resolver.d.ts +14 -0
- package/dist/key-resolver.js +27 -11
- package/dist/plugins/remote-env-host.d.ts +32 -14
- package/dist/plugins/remote-env-host.js +30 -10
- package/dist/plugins/remote-env-local-docker.d.ts +14 -0
- package/dist/plugins/remote-env-local-docker.js +10 -3
- package/dist/plugins/run-store-sql.d.ts +8 -3
- package/dist/plugins/run-store-sql.js +8 -3
- package/dist/run-local.js +2 -2
- package/dist/runtime-caps-resolver.d.ts +1 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/runtime-governance.js +1 -1
- package/dist/sema-registry.d.ts +18 -4
- package/dist/sema-registry.js +17 -3
- package/package.json +2 -2
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* `restart`), computed against the process's BOOT config (not presence, else a refresh would re-fire the same
|
|
4
4
|
* diff and loop-restart). Also owns the model-plane deferral predicates (`planeHasActiveTiers`/
|
|
5
5
|
* `modelPlaneChanged`) main.ts uses to decide whether a hot-apply is safe under a tier-frozen Runner. Split out
|
|
6
|
-
* of `
|
|
6
|
+
* of `facade.ts` (design/158 A13, internal-lossless — the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import { resolveActiveTiers } from "@sema-agent/registry-core";
|
|
9
9
|
import { RUNTIME_GATE_KEYS, runtimeGatePresent, resolveDefaultModelName } from "./apply-effective.js";
|
|
10
|
-
const RESTART_SLICES = ["skills", "mcp", "scenarios", "runtime-gates", "models-tiers"];
|
|
10
|
+
const RESTART_SLICES = ["skills", "mcp", "scenarios", "runtime-gates", "models-tiers", "degrade-route"];
|
|
11
11
|
/** Canonical, key-sorted JSON (array order preserved) so two semantically-equal effective slices fingerprint
|
|
12
12
|
* identically regardless of object key order from the center serializer. */
|
|
13
13
|
function stableStringify(v) {
|
|
@@ -31,7 +31,38 @@ function enabledOnly(rows) {
|
|
|
31
31
|
return null;
|
|
32
32
|
return rows.filter((r) => r.enabled !== false);
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
/** One catalog entry, reduced to the fields whose change is genuinely restart-to-apply. Consumers of a
|
|
35
|
+
* boot-frozen catalog entry read almost every field on the FULLY-DERIVED Model (`toModel`) — id → body.model,
|
|
36
|
+
* baseUrl → the endpoint, provider → which brain, maxTokens/compat/extraBody/input → the request body,
|
|
37
|
+
* apiKeyEnv/sealedApiKey → the credential — so we fingerprint the WHOLE entry minus the three provably HOT
|
|
38
|
+
* fields:
|
|
39
|
+
* - `cost` — refreshed by `mutateInPlace(pricing, buildPricing(config.models))`, and core prefers
|
|
40
|
+
* `deps.pricing[model.id]` over the (possibly frozen) `Model.cost`; `toModel` always emits
|
|
41
|
+
* a cost object, so every center-lane model has a live pricing entry.
|
|
42
|
+
* - `quotaWeight` — refreshed by `mutateInPlace(config.modelQuotaWeights, …)`, read live per burn.
|
|
43
|
+
* - `enabled` — `enabledOnly` already filtered on it, so among the entries that reach here it is
|
|
44
|
+
* `true`-or-absent: two encodings of ONE state. Keeping it would let a center serializer
|
|
45
|
+
* that starts/stops emitting the explicit `true` manufacture a restart out of nothing.
|
|
46
|
+
* Every real enable/disable FLIP changes the filtered SET, so it still signals.
|
|
47
|
+
* A DENY-list (not an allow-list) on purpose: a catalog field added later rides the fingerprint automatically,
|
|
48
|
+
* so the failure mode of drift is a spurious restart, never a silently missed one.
|
|
49
|
+
*
|
|
50
|
+
* Shared by BOTH catalog-entry fingerprints (`degrade-route`, `models-tiers` + `modelPlaneChanged`) since the
|
|
51
|
+
* 复审 2026-07-29 留档: models-tiers had kept all three, so on a tiers-active deployment a pure re-pricing /
|
|
52
|
+
* weight tweak / serializer writing `enabled:true` rolling-restarted the fleet. The two fingerprints must
|
|
53
|
+
* subtract the SAME set — under active tiers `modelPlaneChanged` defers the plane and the refresh loop then
|
|
54
|
+
* FORCE-pushes the `models-tiers` reason for a deferred candidate (boot/config-center.ts R15/R22), so
|
|
55
|
+
* subtracting in only one of them is either no-op (restart still fires) or incoherent (applied yet signalled). */
|
|
56
|
+
function catalogEntryFingerprint(entry) {
|
|
57
|
+
const { cost: _cost, quotaWeight: _quotaWeight, enabled: _enabled, ...route } = entry;
|
|
58
|
+
return route;
|
|
59
|
+
}
|
|
60
|
+
/** The enabled entries of a model plane, each reduced by `catalogEntryFingerprint` (null = no plane published). */
|
|
61
|
+
function planeEntriesFingerprint(rows) {
|
|
62
|
+
const enabled = enabledOnly(rows);
|
|
63
|
+
return enabled ? enabled.map((m) => catalogEntryFingerprint(m)) : null;
|
|
64
|
+
}
|
|
65
|
+
function restartSliceValue(eff, slice, ctx) {
|
|
35
66
|
if (!eff)
|
|
36
67
|
return null;
|
|
37
68
|
switch (slice) {
|
|
@@ -67,7 +98,17 @@ function restartSliceValue(eff, slice) {
|
|
|
67
98
|
// dangling candidates fall through silently here — applyEffective owns the warn.
|
|
68
99
|
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
69
100
|
const def = resolveDefaultModelName(eff, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
70
|
-
|
|
101
|
+
// Entries reduced by the shared deny-list (cost/quotaWeight/enabled are hot — see catalogEntryFingerprint).
|
|
102
|
+
return { models: planeEntriesFingerprint(eff.models?.models), tiers: active, default: def.name };
|
|
103
|
+
}
|
|
104
|
+
case "degrade-route": {
|
|
105
|
+
const to = ctx?.reactiveDegradeTo;
|
|
106
|
+
if (to === undefined)
|
|
107
|
+
return null; // lane off — no degrading brain exists to go stale
|
|
108
|
+
// Present↔absent is itself a change: with no enabled entry named `to`, createBrain composes NO
|
|
109
|
+
// degrading shell at all, so the flip only takes effect at the next boot.
|
|
110
|
+
const target = (enabledOnly(eff.models?.models) ?? []).find((m) => m.name === to);
|
|
111
|
+
return target ? catalogEntryFingerprint(target) : null;
|
|
71
112
|
}
|
|
72
113
|
}
|
|
73
114
|
}
|
|
@@ -75,8 +116,8 @@ function restartSliceValue(eff, slice) {
|
|
|
75
116
|
* nothing baked-at-boot changed (only hot-apply slices moved, or nothing) → no restart needed. An orchestrator
|
|
76
117
|
* can auto rolling-restart on a non-empty result WITHOUT a restart loop, because the comparison is always
|
|
77
118
|
* against boot (a refresh that re-fires the same diff is idempotent, not a fresh trigger). */
|
|
78
|
-
export function restartReasons(boot, current) {
|
|
79
|
-
return RESTART_SLICES.filter((s) => stableStringify(restartSliceValue(boot, s)) !== stableStringify(restartSliceValue(current, s)));
|
|
119
|
+
export function restartReasons(boot, current, ctx) {
|
|
120
|
+
return RESTART_SLICES.filter((s) => stableStringify(restartSliceValue(boot, s, ctx)) !== stableStringify(restartSliceValue(current, s, ctx)));
|
|
80
121
|
}
|
|
81
122
|
/** codex R10 (models-tiers 窗收口): TRUE when the MODEL PLANE (enabled models + active tier table + resolved
|
|
82
123
|
* default) of `next` differs from the last-APPLIED plane `prev`. Under a tier-frozen Runner (tiers non-empty at
|
|
@@ -102,7 +143,10 @@ export function modelPlaneChanged(prev, next) {
|
|
|
102
143
|
const active = e.models ? (resolveActiveTiers(e.models) ?? null) : null;
|
|
103
144
|
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
104
145
|
const def = resolveDefaultModelName(e, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
105
|
-
|
|
146
|
+
// Same reduction as the models-tiers slice — the two MUST subtract the same hot fields (see
|
|
147
|
+
// catalogEntryFingerprint): a deferred candidate force-pushes that slice's reason, so any divergence
|
|
148
|
+
// either re-manufactures the restart we just subtracted or signals a plane that was applied hot.
|
|
149
|
+
return stableStringify({ models: planeEntriesFingerprint(e.models?.models), tiers: active, default: def.name });
|
|
106
150
|
};
|
|
107
151
|
return fp(prev) !== fp(next);
|
|
108
152
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Two small consumption lanes that both project a center manifest domain onto a per-request runtime shape:
|
|
3
3
|
* skills application (`applyCenterSkills` — merge center skills OVER the image baseline, content-addressed
|
|
4
4
|
* fetch+verify+cache) and MCP server resolution (`resolveMcpServers`/`mcpForScenario` — env-NAME ref
|
|
5
|
-
* resolution + per-scenario filtering). Split out of `
|
|
5
|
+
* resolution + per-scenario filtering). Split out of `facade.ts` (design/158 A13, internal-lossless —
|
|
6
6
|
* the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import { skillContentHash } from "@sema-agent/registry-core";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* config-center wire types — the
|
|
2
|
+
* config-center wire types — the config-center `/api/config/effective` payload shapes (`CenterModel`,
|
|
3
3
|
* `CenterTeam`, `CenterSkillManifest`, `CenterMcpServer`, `EffectiveConfig`) plus the per-principal
|
|
4
4
|
* execution/session-mirror ruling shapes riding the caps view (`ExecutionRuling`/`SessionMirrorRuling`).
|
|
5
|
-
* Pure type/interface declarations — no runtime logic. Split out of `
|
|
5
|
+
* Pure type/interface declarations — no runtime logic. Split out of `facade.ts` (design/158 A13,
|
|
6
6
|
* internal-lossless: the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import type { CollabTemplateWire } from "../capabilities/collab-wire.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileConfigStore } from "@sema-agent/registry-core/node";
|
|
2
2
|
import { type EffectiveConfig as AgentConfigEffective } from "@sema-agent/registry-core";
|
|
3
|
-
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./
|
|
4
|
-
/** Result of a `fetchEffective` — EXACTLY
|
|
3
|
+
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./config-center/facade.js";
|
|
4
|
+
/** Result of a `fetchEffective` — EXACTLY the config-center's return: the effective config + its etag, or
|
|
5
5
|
* `null` for "unchanged" (remote 304; local: the version matched the caller's prior etag).
|
|
6
6
|
* `domainErrors`([898] registry-core 0.10.12 tolerant seam,local lane only):catalog 域坏文件不再
|
|
7
7
|
* 连坐全包回落 env——坏域按该域 schema default 落+错误单列,好域照常生效;caller(main.ts)对每条打
|
|
@@ -29,12 +29,12 @@ export declare const BOOT_FETCH_DEFERRED: unique symbol;
|
|
|
29
29
|
export declare function raceBootFetch<T>(fetch: Promise<T>, budgetMs: number): Promise<T | typeof BOOT_FETCH_DEFERRED>;
|
|
30
30
|
/**
|
|
31
31
|
* The two transport calls the service makes to obtain config, behind a backend-selectable seam. Both
|
|
32
|
-
* signatures and return shapes match `
|
|
32
|
+
* signatures and return shapes match `facade.fetchEffective` / `facade.fetchSkillContent`
|
|
33
33
|
* EXACTLY so that, ONCE WIRED, the caller (main.ts) could swap remote↔local without touching applyEffective
|
|
34
34
|
* et al. (main.ts does not swap on this seam yet — see the NOT-YET-WIRED note at the top of this module).
|
|
35
35
|
*/
|
|
36
36
|
export interface ConfigProvider {
|
|
37
|
-
/** "remote" =
|
|
37
|
+
/** "remote" = config-center HTTP; "local" = on-disk FileConfigStore. */
|
|
38
38
|
readonly kind: "remote" | "local";
|
|
39
39
|
/**
|
|
40
40
|
* Pull the effective config. `etag` is the caller's last-seen version token (`if-none-match` on remote;
|
|
@@ -72,9 +72,9 @@ export interface ConfigProviderInput {
|
|
|
72
72
|
localDir?: string;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
* RemoteConfigProvider — DELEGATES to the exported
|
|
75
|
+
* RemoteConfigProvider — DELEGATES to the exported config-center HTTP functions. No HTTP is reimplemented;
|
|
76
76
|
* baseUrl/token/worker are bound once and passed through. The two delegate fns are injectable (defaulting
|
|
77
|
-
* to the real
|
|
77
|
+
* to the real config-center exports) so a unit test can assert delegation without module-mocking.
|
|
78
78
|
*/
|
|
79
79
|
export declare class RemoteConfigProvider implements ConfigProvider {
|
|
80
80
|
private readonly cc;
|
|
@@ -120,7 +120,7 @@ export declare class LocalConfigProvider implements ConfigProvider {
|
|
|
120
120
|
export declare function mapToServiceEffective(eff: AgentConfigEffective, version: number): EffectiveConfig;
|
|
121
121
|
/**
|
|
122
122
|
* Pick the backend off a CLOSED `provider` enum (`local` | `remote` | undefined): LOCAL when
|
|
123
|
-
* `CONFIG_PROVIDER=local`, OR when no
|
|
123
|
+
* `CONFIG_PROVIDER=local`, OR when no config-center URL is configured (a lone box with config.d/ on disk
|
|
124
124
|
* and no control plane). REMOTE otherwise (a configured center URL, the default fleet posture). A
|
|
125
125
|
* `CONFIG_PROVIDER=remote` with no URL is a misconfiguration — we fall through to local rather than
|
|
126
126
|
* constructing a remote provider with no endpoint (the caller would have skipped the remote pull entirely
|
package/dist/config-provider.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ConfigProvider — the dual-mode seam over WHERE the effective config comes from (see
|
|
3
3
|
* sema-registry docs/DUAL-MODE-DESIGN.md §4).
|
|
4
4
|
*
|
|
5
|
-
* The service consumes
|
|
5
|
+
* The service consumes the config-center through EXACTLY two transport calls — `fetchEffective` (pull the
|
|
6
6
|
* effective config, ETag-conditional) and `fetchSkillContent` (lazy-pull one skill body by content hash).
|
|
7
7
|
* Everything downstream (applyEffective / applyRuntimeGates / resolveMcpServers / mcpForScenario / the
|
|
8
8
|
* skill overlay) is transport-agnostic: it operates on the returned `EffectiveConfig` / skill string.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* This module factors those two calls behind a `ConfigProvider` interface with two backends:
|
|
18
18
|
*
|
|
19
19
|
* - {@link RemoteConfigProvider} — the existing behaviour. DELEGATES verbatim to the exported
|
|
20
|
-
* `
|
|
20
|
+
* `facade.fetchEffective` / `facade.fetchSkillContent` (HTTP + Bearer + ETag + hash
|
|
21
21
|
* verify). No HTTP is reimplemented here; this is a thin wrapper that binds baseUrl/token/worker.
|
|
22
22
|
*
|
|
23
23
|
* - {@link LocalConfigProvider} — reads the SAME config contract from the local filesystem via
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*
|
|
31
31
|
* SECRET BOUNDARY (unchanged in both modes): the config plane carries only env-NAME refs — `apiKeyEnv`,
|
|
32
32
|
* `envRefs`/`headerRefs`, `tokenEnv`. The real secret VALUE is never stored in config.d/<domain>.json and
|
|
33
|
-
* never read by this module; it is resolved from `process.env` downstream (
|
|
33
|
+
* never read by this module; it is resolved from `process.env` downstream (config-center/facade.ts
|
|
34
34
|
* `applyEffective`/`resolveMcpServers`). The local store is symmetric with the remote center on this: it
|
|
35
35
|
* ships NAMEs only. (Unit test asserts the local path resolves env-NAME→value via process.env and never
|
|
36
36
|
* persists a value.)
|
|
@@ -39,7 +39,7 @@ import { createHmac, randomBytes } from "node:crypto";
|
|
|
39
39
|
import { FileConfigStore } from "@sema-agent/registry-core/node";
|
|
40
40
|
import { findSkillContent, skillContentHash, refIntegrityIssues, siblingResolver, } from "@sema-agent/registry-core";
|
|
41
41
|
import { redactSecrets } from "./trace/redact.js";
|
|
42
|
-
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, ConfigCenterHttpError, } from "./
|
|
42
|
+
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, ConfigCenterHttpError, } from "./config-center/facade.js";
|
|
43
43
|
import { createLogger } from "./observability/logger.js";
|
|
44
44
|
const logger = createLogger();
|
|
45
45
|
/**
|
|
@@ -79,9 +79,9 @@ export function raceBootFetch(fetch, budgetMs) {
|
|
|
79
79
|
return Promise.race([fetch.finally(() => clearTimeout(timer)), budget]);
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* RemoteConfigProvider — DELEGATES to the exported
|
|
82
|
+
* RemoteConfigProvider — DELEGATES to the exported config-center HTTP functions. No HTTP is reimplemented;
|
|
83
83
|
* baseUrl/token/worker are bound once and passed through. The two delegate fns are injectable (defaulting
|
|
84
|
-
* to the real
|
|
84
|
+
* to the real config-center exports) so a unit test can assert delegation without module-mocking.
|
|
85
85
|
*/
|
|
86
86
|
export class RemoteConfigProvider {
|
|
87
87
|
cc;
|
|
@@ -93,7 +93,7 @@ export class RemoteConfigProvider {
|
|
|
93
93
|
}
|
|
94
94
|
fetchEffective(etag) {
|
|
95
95
|
const fn = this.deps.fetchEffective ?? remoteFetchEffective;
|
|
96
|
-
// 5th arg = worker scope; transport (fetchImpl) stays
|
|
96
|
+
// 5th arg = worker scope; transport (fetchImpl) stays the config-center's default.
|
|
97
97
|
return fn(this.cc.baseUrl, this.cc.token, etag, undefined, this.cc.worker);
|
|
98
98
|
}
|
|
99
99
|
async fetchSkillContent(contentHash) {
|
|
@@ -219,7 +219,7 @@ export function mapToServiceEffective(eff, version) {
|
|
|
219
219
|
// [874] registry-core 0.10.11:池结构级 `default?`(catalog name 目录键,与 roles.default 角色词两回事)。
|
|
220
220
|
// 本 mapper 是 CLOSED 投影——不直通则本地 lane 静默丢键(autoCompactTokens 同类病),applyEffective 的
|
|
221
221
|
// resolveDefaultModelName 第一优先级在本地 lane 永远点不亮。写面悬空 ref 由 registry-core superRefine
|
|
222
|
-
// fail-loud;存量/未经 schema 的路径仍由消费方 warn 降级(
|
|
222
|
+
// fail-loud;存量/未经 schema 的路径仍由消费方 warn 降级(config-center/facade.ts resolveDefaultModelName)。
|
|
223
223
|
...(eff.models.default !== undefined ? { default: eff.models.default } : {}),
|
|
224
224
|
...(eff.models.atModelAllowlist !== undefined ? { atModelAllowlist: eff.models.atModelAllowlist } : {}),
|
|
225
225
|
// registry-core 0.9.0 档位组: verbatim passthrough — applyEffective's resolveActiveTiers reads
|
|
@@ -322,7 +322,7 @@ export function mapToServiceEffective(eff, version) {
|
|
|
322
322
|
}
|
|
323
323
|
/**
|
|
324
324
|
* Pick the backend off a CLOSED `provider` enum (`local` | `remote` | undefined): LOCAL when
|
|
325
|
-
* `CONFIG_PROVIDER=local`, OR when no
|
|
325
|
+
* `CONFIG_PROVIDER=local`, OR when no config-center URL is configured (a lone box with config.d/ on disk
|
|
326
326
|
* and no control plane). REMOTE otherwise (a configured center URL, the default fleet posture). A
|
|
327
327
|
* `CONFIG_PROVIDER=remote` with no URL is a misconfiguration — we fall through to local rather than
|
|
328
328
|
* constructing a remote provider with no endpoint (the caller would have skipped the remote pull entirely
|
package/dist/config.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat
|
|
|
8
8
|
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
9
9
|
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
10
10
|
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
|
11
|
-
* config overlay (
|
|
11
|
+
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
12
12
|
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
13
13
|
export declare function parseAutonomy(raw: string | undefined): Autonomy | undefined;
|
|
14
14
|
export declare function drainConfigWarnings(): Array<{
|
package/dist/config.js
CHANGED
|
@@ -15,7 +15,7 @@ const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
|
15
15
|
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
16
16
|
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
17
17
|
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
|
18
|
-
* config overlay (
|
|
18
|
+
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
19
19
|
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
20
20
|
export function parseAutonomy(raw) {
|
|
21
21
|
const v = raw?.trim();
|
|
@@ -534,8 +534,8 @@ function parseModelDomain() {
|
|
|
534
534
|
// when body.images hits a text-only model) AND the vision flag on GET /v1/models. The env-lane default model
|
|
535
535
|
// declares vision via MODEL_VISION — DEFAULT "true" keeps the historical image-capable behavior, so set
|
|
536
536
|
// MODEL_VISION=false for a text-only model (e.g. deepseek) to make the precheck actually fire instead of letting
|
|
537
|
-
// images透传 to an opaque downstream-gateway 400. (The centralized
|
|
538
|
-
// per-model vision flag — see
|
|
537
|
+
// images透传 to an opaque downstream-gateway 400. (The centralized config-center lane derives input from its own
|
|
538
|
+
// per-model vision flag — see config-center/facade.ts.)
|
|
539
539
|
input: env("MODEL_VISION", "true") === "true" ? ["text", "image"] : ["text"],
|
|
540
540
|
// Per-1M-token USD pricing (core `modelCostToPricing`: input→inputPer1M, …). Default 0 keeps the
|
|
541
541
|
// historical behavior (spend reads $0) — set MODEL_COST_* on a deploy to make `model_cost_micro_usd`
|
|
@@ -624,7 +624,7 @@ function parseModelDomain() {
|
|
|
624
624
|
// Static catalog audit for this batch: ZERO baked 1M entries exist (env lane defaults 262144; center lane
|
|
625
625
|
// inherits/declares per roster) — so this is a DERIVATION on the declared window, not a data edit: any
|
|
626
626
|
// deployment that sets MODEL_CONTEXT_WINDOW>=1e6 (or a center roster with a 1M contextWindow —
|
|
627
|
-
//
|
|
627
|
+
// config-center/facade.ts applies the same helper) gets the field automatically. <1M ⇒ field ABSENT (core's plain
|
|
628
628
|
// W-33000/0.7W geometry already matches CC there).
|
|
629
629
|
applyAutoCompactWindow(model);
|
|
630
630
|
if (cheapModel)
|
|
@@ -849,7 +849,7 @@ function parseMemoryDomain(ctx) {
|
|
|
849
849
|
memoryEngineBackend,
|
|
850
850
|
memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
|
|
851
851
|
...(memorySync ? { memorySync } : {}),
|
|
852
|
-
projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (legacy PROJECT_MEMORY_DISABLED
|
|
852
|
+
projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (3.0.0 起 legacy PROJECT_MEMORY_DISABLED = fail-loud 墓碑)
|
|
853
853
|
syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
|
|
854
854
|
};
|
|
855
855
|
}
|
|
@@ -209,7 +209,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
209
209
|
// (yields a task, runs no model — see the billable-route classifier) → NO lease gate, else an exhausted
|
|
210
210
|
// tenant could not stop its own spending.
|
|
211
211
|
if (!deps.runStore) {
|
|
212
|
-
sendError(res, 501, "capability.run_store_required", "preemption requires
|
|
212
|
+
sendError(res, 501, "capability.run_store_required", "preemption requires a durable run store (DB_BACKEND=mysql|pg)");
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
// requirePrincipal parity with the sibling mutating endpoints (runOwnerOk / cancel / runs): 401 before the
|
|
@@ -282,7 +282,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
282
282
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
283
283
|
return; // 🔴 复审 C2:lease admitted INSIDE driveResumeIntoRunLog on the CHECKPOINT-OWNER principal (the billed tenant), not the request principal — a cross-tenant operator resume must charge the owner's lease, not the operator's. resume hits TiDB + runs the model
|
|
284
284
|
if (!deps.runStore) {
|
|
285
|
-
sendError(res, 501, "capability.run_store_required", "resume requires
|
|
285
|
+
sendError(res, 501, "capability.run_store_required", "resume requires a durable run store (DB_BACKEND=mysql|pg)");
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
@@ -319,7 +319,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
319
319
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
320
320
|
return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB + runs the model (approve/edit)
|
|
321
321
|
if (!deps.runStore) {
|
|
322
|
-
sendError(res, 501, "capability.run_store_required", "plan_review requires
|
|
322
|
+
sendError(res, 501, "capability.run_store_required", "plan_review requires a durable run store (DB_BACKEND=mysql|pg)");
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
325
|
if (deps.config.requirePrincipal && principal === undefined) {
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -93,7 +93,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
93
93
|
// Async run: create + return ids immediately, execute in the background.
|
|
94
94
|
if (req.method === "POST" && url === "/v1/runs") {
|
|
95
95
|
if (!deps.runStore) {
|
|
96
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
96
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg) (SESSION_BACKEND=tidb)");
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
// Idempotency-Key dedup (center blocker): a retried create returns the SAME taskId instead of starting a
|
|
@@ -222,7 +222,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
222
222
|
const runMatch = req.method === "GET" ? RUN_ID_RE.exec(url) : null;
|
|
223
223
|
if (runMatch) {
|
|
224
224
|
if (!deps.runStore) {
|
|
225
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
225
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
const taskId = runMatch[1];
|
|
@@ -310,7 +310,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
310
310
|
const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
|
|
311
311
|
if (cancelMatch) {
|
|
312
312
|
if (!deps.runStore) {
|
|
313
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
313
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
316
316
|
// Principal check BEFORE the lookup (parity with GET /v1/runs/:id): no 404-vs-401 existence oracle.
|
|
@@ -496,7 +496,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
496
496
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
497
497
|
return; // mutating + hits TiDB / runs the model
|
|
498
498
|
if (!deps.runStore) {
|
|
499
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
499
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
500
500
|
return;
|
|
501
501
|
}
|
|
502
502
|
// Per-tenant identity MUST come from gatedPrincipal (direct-door secure single point), NOT principalFrom:
|
|
@@ -713,7 +713,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
713
713
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
714
714
|
return; // mutating + runs the model (compaction summarizes)
|
|
715
715
|
if (!deps.runStore) {
|
|
716
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
716
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
717
717
|
return;
|
|
718
718
|
}
|
|
719
719
|
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
@@ -818,7 +818,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
818
818
|
if (rateLimited(req, res))
|
|
819
819
|
return; // mutating, but runs no model (no quota gate — parity with cancel, not steer)
|
|
820
820
|
if (!deps.runStore) {
|
|
821
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
821
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
822
822
|
return;
|
|
823
823
|
}
|
|
824
824
|
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
@@ -1183,7 +1183,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1183
1183
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
1184
1184
|
return; // mutating + drives a model agent
|
|
1185
1185
|
if (!deps.runStore) {
|
|
1186
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
1186
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
1187
1187
|
return;
|
|
1188
1188
|
}
|
|
1189
1189
|
// Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — `trusted`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { materializeMcpTools, sanitizePathComponent, SessionPolicyError } from "@sema-agent/core";
|
|
2
|
-
import { mcpForScenario } from "../../
|
|
2
|
+
import { mcpForScenario } from "../../config-center/facade.js";
|
|
3
3
|
import { isUuidV7, isUuidShape } from "../../security.js";
|
|
4
4
|
import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
@@ -36,7 +36,7 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
if (!deps.runStore) {
|
|
39
|
-
sendError(res, 501, "capability.run_store_required", "trace API requires
|
|
39
|
+
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg) (SESSION_BACKEND=tidb)");
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
@@ -56,7 +56,7 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
if (!deps.runStore) {
|
|
59
|
-
sendError(res, 501, "capability.run_store_required", "trace API requires
|
|
59
|
+
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg) (SESSION_BACKEND=tidb)");
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
const query = new URL(req.url ?? "", "http://x").searchParams;
|
package/dist/http/server.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { IncomingMessage } from "node:http";
|
|
|
3
3
|
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
|
-
import { type RestartSignal, type SessionMirrorRuling } from "../
|
|
6
|
+
import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
|
|
7
7
|
import { type OwnerAwareSessionStore } from "../security.js";
|
|
8
8
|
import type { RunStore, ApprovalStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, StoreBackend } from "../plugins/store-backend.js";
|
|
9
9
|
import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js";
|
package/dist/http/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, canonicalToolName, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow } from "@sema-agent/core"; // canonicalToolName = core single-source (1.162; replaced the transitional service mirror)
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
6
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
|
-
import {} from "../
|
|
7
|
+
import {} from "../config-center/facade.js";
|
|
8
8
|
import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, verifyDirectDoorProof } from "../security.js";
|
|
9
9
|
import { exportSession, importSession } from "../session-sync.js";
|
|
10
10
|
import {} from "../memory-sync.js";
|
|
@@ -407,7 +407,7 @@ export function createHttpServer(rawDeps) {
|
|
|
407
407
|
return;
|
|
408
408
|
}
|
|
409
409
|
// Metrics (read-only): authorized by EITHER the full authToken OR a read-only metricsToken — so a
|
|
410
|
-
// control plane (
|
|
410
|
+
// control plane (config-center) can pull metrics fleet-wide with one token, never holding each
|
|
411
411
|
// worker's full authToken. Handled BEFORE the global gate so metricsToken-only callers aren't 401'd.
|
|
412
412
|
if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
|
|
413
413
|
if (!deps.metrics) {
|
|
@@ -495,7 +495,7 @@ export function createHttpServer(rawDeps) {
|
|
|
495
495
|
reqState.source = source;
|
|
496
496
|
// Mandatory service token on submission endpoints (center: a worker MUST validate a service-to-service
|
|
497
497
|
// token on task creation — not "if auth is configured" — else anything in-cluster can submit & bill
|
|
498
|
-
// directly, bypassing
|
|
498
|
+
// directly, bypassing config-center's auth/audit). Fail-closed: the POST task/run/leader endpoints refuse
|
|
499
499
|
// when no authToken is set, unless explicitly opted out for local dev (ALLOW_UNAUTHED_WRITES=true).
|
|
500
500
|
// The bake door (POST /v1/images/bakes*) is build-host-RCE-capable and authed by the Bearer-token-no-cookie
|
|
501
501
|
// model (§P2.4b) — it MUST also refuse when no service token is configured (else a forged principal header
|
package/dist/key-resolver.d.ts
CHANGED
|
@@ -26,4 +26,18 @@ import { type SealedKeyPoison } from "./sealed-key.js";
|
|
|
26
26
|
export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: Model) => Promise<{
|
|
27
27
|
apiKey: string;
|
|
28
28
|
} | undefined>) | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The ONE per-model key chain, synchronous: **sealed custody → env-NAME reference → (caller's) gateway key**,
|
|
31
|
+
* with a poisoned sealed entry THROWING instead of returning undefined. `createKeyResolver` is the async
|
|
32
|
+
* `getApiKeyAndHeaders` face of exactly this function — every other consumer must call it rather than
|
|
33
|
+
* re-implement a subset of the chain.
|
|
34
|
+
*
|
|
35
|
+
* 复审 2026-07-29(同族缺口):`brain.ts` 的反应式降级 hop 曾只查 `modelApiKeyEnv` —— sealed-box 托管密钥的
|
|
36
|
+
* 降级目标(按互斥契约**永不**落 modelApiKeyEnv)于是静默拿网关 key 打自己的网关(错 key 打上游),毒丸更
|
|
37
|
+
* 被当成"没配 key"。链只有一条,重复实现就会长出这种半条链的偏差,故此处抽出同步内核共用。
|
|
38
|
+
*
|
|
39
|
+
* `undefined` = this model has no per-model key ⇒ the CALLER's gateway/default credential applies (the
|
|
40
|
+
* additive contract). An empty env value counts as unset (unchanged from the original inline chain).
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveModelApiKey(modelName: string, modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): string | undefined;
|
|
29
43
|
//# sourceMappingURL=key-resolver.d.ts.map
|
package/dist/key-resolver.js
CHANGED
|
@@ -26,18 +26,34 @@ export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKey
|
|
|
26
26
|
if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
|
|
27
27
|
return undefined;
|
|
28
28
|
return async (model) => {
|
|
29
|
-
const
|
|
30
|
-
// Poison pill: the operator configured a sealed key for this model but it cannot be unsealed —
|
|
31
|
-
// fail the brain call LOUD (never undefined = never the gateway fallback).
|
|
32
|
-
if (isSealedKeyPoison(sealed))
|
|
33
|
-
throw new SealedKeyPoisonedError(model.name, sealed);
|
|
34
|
-
if (sealed)
|
|
35
|
-
return { apiKey: sealed }; // sealed-box custody wins (registry-core: sealed 压过 apiKeyEnv)
|
|
36
|
-
const envName = modelApiKeyEnv[model.name];
|
|
37
|
-
if (!envName)
|
|
38
|
-
return undefined; // model has no per-model key → core uses the gateway key
|
|
39
|
-
const apiKey = env[envName];
|
|
29
|
+
const apiKey = resolveModelApiKey(model.name, modelApiKeyEnv, env, modelApiKeys);
|
|
40
30
|
return apiKey ? { apiKey } : undefined;
|
|
41
31
|
};
|
|
42
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The ONE per-model key chain, synchronous: **sealed custody → env-NAME reference → (caller's) gateway key**,
|
|
35
|
+
* with a poisoned sealed entry THROWING instead of returning undefined. `createKeyResolver` is the async
|
|
36
|
+
* `getApiKeyAndHeaders` face of exactly this function — every other consumer must call it rather than
|
|
37
|
+
* re-implement a subset of the chain.
|
|
38
|
+
*
|
|
39
|
+
* 复审 2026-07-29(同族缺口):`brain.ts` 的反应式降级 hop 曾只查 `modelApiKeyEnv` —— sealed-box 托管密钥的
|
|
40
|
+
* 降级目标(按互斥契约**永不**落 modelApiKeyEnv)于是静默拿网关 key 打自己的网关(错 key 打上游),毒丸更
|
|
41
|
+
* 被当成"没配 key"。链只有一条,重复实现就会长出这种半条链的偏差,故此处抽出同步内核共用。
|
|
42
|
+
*
|
|
43
|
+
* `undefined` = this model has no per-model key ⇒ the CALLER's gateway/default credential applies (the
|
|
44
|
+
* additive contract). An empty env value counts as unset (unchanged from the original inline chain).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveModelApiKey(modelName, modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
|
|
47
|
+
const sealed = modelApiKeys[modelName];
|
|
48
|
+
// Poison pill: the operator configured a sealed key for this model but it cannot be unsealed —
|
|
49
|
+
// fail the brain call LOUD (never undefined = never the gateway fallback).
|
|
50
|
+
if (isSealedKeyPoison(sealed))
|
|
51
|
+
throw new SealedKeyPoisonedError(modelName, sealed);
|
|
52
|
+
if (sealed)
|
|
53
|
+
return sealed; // sealed-box custody wins (registry-core: sealed 压过 apiKeyEnv)
|
|
54
|
+
const envName = modelApiKeyEnv[modelName];
|
|
55
|
+
if (!envName)
|
|
56
|
+
return undefined; // model has no per-model key → core uses the gateway key
|
|
57
|
+
return env[envName] || undefined;
|
|
58
|
+
}
|
|
43
59
|
//# sourceMappingURL=key-resolver.js.map
|
|
@@ -280,22 +280,40 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
280
280
|
* `ctx.sessionId` into the workspace dir id so two concurrent tasks never share a working directory.
|
|
281
281
|
*/
|
|
282
282
|
export declare function hostExecutionEnvFactory(config?: HostEnvConfig): ExecutionEnvFactory;
|
|
283
|
-
export { localDockerExecutionEnvFactory, type LocalDockerEnvConfig, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
export { localDockerExecutionEnvFactory, type LocalDockerEnvConfig, type LocalDockerEnvDeps, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
284
|
+
import { type LocalDockerEnvConfig, type LocalDockerEnvDeps } from "./remote-env-local-docker.js";
|
|
285
|
+
/**
|
|
286
|
+
* Config for the `remote-docker` provider: run each task in a container on a REMOTE docker daemon
|
|
287
|
+
* (DUAL-MODE-DESIGN §5 — "a user-specified REMOTE host's docker"; registry-core `RemoteExecSpec`).
|
|
288
|
+
*
|
|
289
|
+
* It is {@link LocalDockerEnvConfig} with the endpoint made REQUIRED — that is the whole difference, by design:
|
|
290
|
+
* the container adapter is already daemon-agnostic (workspace `mkdir`'d INSIDE the container, never a host
|
|
291
|
+
* bind-mount; bytes over `docker cp` / `docker exec cat`, which stream through the daemon API), and the endpoint
|
|
292
|
+
* rides as `-H <dockerHost>` on every invocation. So every local-docker knob (memory/cpus/pidsLimit/dropAllCaps/
|
|
293
|
+
* network/timeouts/env/dockerPath) applies unchanged to the remote lane.
|
|
294
|
+
*/
|
|
295
|
+
export interface RemoteDockerEnvConfig extends Omit<LocalDockerEnvConfig, "dockerHost" | "providerLabel"> {
|
|
296
|
+
/** Remote docker daemon endpoint (e.g. `tcp://host:2376` or `ssh://user@host`). REQUIRED for remote-docker. */
|
|
292
297
|
dockerHost: string;
|
|
293
|
-
/** Out-of-band env injected into the container (secrets resolved by the control plane). */
|
|
294
|
-
env?: Record<string, string>;
|
|
295
298
|
}
|
|
296
299
|
/**
|
|
297
|
-
* `
|
|
298
|
-
*
|
|
300
|
+
* `ExecutionEnvFactory` for the TOC `remote-docker` backend (DUAL-MODE-DESIGN §5): a per-task container on
|
|
301
|
+
* SOMEONE ELSE'S docker daemon (offload — the operator's box stays free; isolation:true, suspendable:false).
|
|
302
|
+
* Same lifetime contract as local-docker: unconnected/lazy, one container per task, `destroy()` = `docker rm -f`.
|
|
303
|
+
*
|
|
304
|
+
* 🔴 **Endpoint credentials are the docker CLI's own** — this adapter deliberately carries none. The docker CLI
|
|
305
|
+
* reads `DOCKER_CERT_PATH` + `DOCKER_TLS_VERIFY` from the worker's environment (inherited by every spawn) for a
|
|
306
|
+
* `tcp://` + TLS daemon, and uses `ssh-agent` / `~/.ssh/config` for an `ssh://` one. That keeps key material out
|
|
307
|
+
* of this process's config objects entirely. Consequence to know: `tcp://…:2376` WITHOUT those env vars fails
|
|
308
|
+
* loud at connect (the daemon refuses plaintext) — it does not silently downgrade; a plaintext `tcp://…:2375`
|
|
309
|
+
* daemon is unauthenticated and remains the operator's decision, exactly as in the registry-core schema.
|
|
310
|
+
* (The registry-core `remote-docker` arm additionally models `tlsCertEnv`/`tlsKeyEnv`/`tlsCaEnv`/`sshKeyEnv` =
|
|
311
|
+
* env-NAMEs the control plane resolves to PEM BYTES. Materializing those bytes to short-lived 0600 files and
|
|
312
|
+
* passing `--tlsverify --tlscert/--tlskey/--tlscacert` is the remaining config-surface follow-up, together with
|
|
313
|
+
* the `provider:"remote-docker"` arm on `AppConfig.remoteExec` — see the note on that union in config-types.ts.)
|
|
314
|
+
*
|
|
315
|
+
* Throws at factory-BUILD time on a missing/blank endpoint: a "remote" lane with no endpoint would otherwise
|
|
316
|
+
* quietly become the LOCAL daemon — an isolation-topology surprise, so it fails loud instead.
|
|
299
317
|
*/
|
|
300
|
-
export declare function remoteDockerExecutionEnvFactory(
|
|
318
|
+
export declare function remoteDockerExecutionEnvFactory(config: RemoteDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
|
|
301
319
|
//# sourceMappingURL=remote-env-host.d.ts.map
|