@sema-agent/settings-schema 1.0.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/CHANGELOG.md +728 -0
- package/LICENSE +103 -0
- package/README.md +129 -0
- package/dist/api/auth-bridge.d.ts +331 -0
- package/dist/api/auth-bridge.js +210 -0
- package/dist/api/auth.d.ts +216 -0
- package/dist/api/auth.js +138 -0
- package/dist/api/scopes.d.ts +344 -0
- package/dist/api/scopes.js +222 -0
- package/dist/api/wire.d.ts +60 -0
- package/dist/api/wire.js +89 -0
- package/dist/bundle.d.ts +13 -0
- package/dist/bundle.js +67 -0
- package/dist/config-fns.d.ts +318 -0
- package/dist/config-fns.js +472 -0
- package/dist/cross-domain.d.ts +34 -0
- package/dist/cross-domain.js +118 -0
- package/dist/file-edit.d.ts +36 -0
- package/dist/file-edit.js +125 -0
- package/dist/file-store.d.ts +89 -0
- package/dist/file-store.js +238 -0
- package/dist/fleet.d.ts +498 -0
- package/dist/fleet.js +317 -0
- package/dist/hash.d.ts +32 -0
- package/dist/hash.js +59 -0
- package/dist/hooks.d.ts +5477 -0
- package/dist/hooks.js +627 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +26 -0
- package/dist/local-load.d.ts +42 -0
- package/dist/local-load.js +172 -0
- package/dist/migrate.d.ts +255 -0
- package/dist/migrate.js +542 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.js +11 -0
- package/dist/reader.d.ts +18 -0
- package/dist/reader.js +1 -0
- package/dist/remote-exec.d.ts +274 -0
- package/dist/remote-exec.js +182 -0
- package/dist/resolve-roster.d.ts +28 -0
- package/dist/resolve-roster.js +108 -0
- package/dist/safety-merge-spec.d.ts +327 -0
- package/dist/safety-merge-spec.js +70 -0
- package/dist/scheduler-store-node.d.ts +72 -0
- package/dist/scheduler-store-node.js +119 -0
- package/dist/scheduler-store.d.ts +67 -0
- package/dist/scheduler-store.js +89 -0
- package/dist/secret-refs.d.ts +33 -0
- package/dist/secret-refs.js +48 -0
- package/dist/sha256.d.ts +16 -0
- package/dist/sha256.js +114 -0
- package/dist/skills-manifest.d.ts +12 -0
- package/dist/skills-manifest.js +54 -0
- package/dist/types.d.ts +13560 -0
- package/dist/types.js +2118 -0
- package/package.json +138 -0
package/dist/api/wire.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api/wire` — the ZERO-DEPENDENCY wire subset of the auth/scopes contracts (server AI 接手首车,
|
|
3
|
+
* 2026-07-27,跨仓 candidate 落地):endpoint paths, protocol constants and PURE helper functions,
|
|
4
|
+
* with **no zod import** — so a zero-runtime-dependency client (e.g. `@sema-agent/sdk/registry`) can
|
|
5
|
+
* import these at RUNTIME instead of value-copying them behind a same-source anchor test.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ 拆分纪律:这里只放**纯**件(常量/纯函数/字面量类型)。zod schema 留在 `api/auth.ts` /
|
|
8
|
+
* `api/scopes.ts`(它们 re-export 本文件 ⇒ 既有导入面字节兼容,谁都不用改)。给本文件加东西前
|
|
9
|
+
* 自问:它 import 了任何东西吗?——答案必须是 no(test/wire-purity 打包门盯着)。
|
|
10
|
+
*/
|
|
11
|
+
// ── auth: endpoint paths + protocol constants(api/auth.ts 原件,shipped values 冻结)─────────────────
|
|
12
|
+
/** The five auth endpoints, relative to the registry origin. */
|
|
13
|
+
export const AUTH_V1_PATHS = {
|
|
14
|
+
deviceCode: "/api/v1/auth/device/code",
|
|
15
|
+
deviceToken: "/api/v1/auth/device/token",
|
|
16
|
+
deviceApprove: "/api/v1/auth/device/approve",
|
|
17
|
+
tokenRefresh: "/api/v1/auth/token/refresh",
|
|
18
|
+
logout: "/api/v1/auth/logout",
|
|
19
|
+
};
|
|
20
|
+
/** The RFC 8628 grant_type the device token endpoint requires. */
|
|
21
|
+
export const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
22
|
+
/** Minimum seconds between token-endpoint polls (RFC 8628 `interval`); polling faster → `slow_down`. */
|
|
23
|
+
export const DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
24
|
+
/** Device-code handshake lifetime (seconds) — the user has 15 minutes to open /activate and approve. */
|
|
25
|
+
export const DEVICE_CODE_TTL_SECONDS = 15 * 60;
|
|
26
|
+
/** Access-token (RS256 JWT) lifetime in seconds — 1h, re-minted via refresh. */
|
|
27
|
+
export const ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
28
|
+
/** Refresh-grant lifetime (seconds) — 30d SLIDING: every rotation opens a fresh 30d window. */
|
|
29
|
+
export const REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
30
|
+
/** Every error code the /api/v1/auth/* endpoints answer (HTTP 400 unless noted). */
|
|
31
|
+
export const OAUTH_ERROR_CODES = [
|
|
32
|
+
/** device/token: the user has not approved (or denied) the handshake yet — keep polling. */
|
|
33
|
+
"authorization_pending",
|
|
34
|
+
/** device/token: polling faster than `interval` — RFC 8628 §3.5: ADD 5 seconds to the interval. */
|
|
35
|
+
"slow_down",
|
|
36
|
+
/** device/token: handshake expired, unknown, or already exchanged — restart the flow.
|
|
37
|
+
* (Deliberately covers "never existed": the server does not disclose handshake existence.) */
|
|
38
|
+
"expired_token",
|
|
39
|
+
/** device/token: the user explicitly denied the handshake — stop polling, do not restart silently. */
|
|
40
|
+
"access_denied",
|
|
41
|
+
/** any endpoint: malformed body / missing required field / wrong content type. */
|
|
42
|
+
"invalid_request",
|
|
43
|
+
/** device/token: grant_type is not DEVICE_GRANT_TYPE. */
|
|
44
|
+
"unsupported_grant_type",
|
|
45
|
+
/** token/refresh + device/approve: unknown / expired / revoked / replayed token or code — no detail. */
|
|
46
|
+
"invalid_grant",
|
|
47
|
+
/** SSO not configured on the registry (HTTP 503) — a deployment problem, not a client one. */
|
|
48
|
+
"server_error",
|
|
49
|
+
];
|
|
50
|
+
/** Handshake states a consent-screen lookup can see. (`approved` rows vanish once exchanged/consumed.)
|
|
51
|
+
* `denied` is contract-reserved for an explicit deny button (pairs with the `access_denied` poll error);
|
|
52
|
+
* the shipped M1 store carries pending|approved only — adding deny later is implementation, not schema. */
|
|
53
|
+
export const DEVICE_AUTH_STATUSES = ["pending", "approved", "denied"];
|
|
54
|
+
// ── auth: pure client-side helpers(api/auth.ts 原件)────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Canonicalize a human-typed user code to the wire shape "XXXX-XXXX": uppercase, dashes/spaces optional.
|
|
57
|
+
* null = not a plausible code (wrong length/charset after stripping separators). Same normalization the
|
|
58
|
+
* server applies — run it client-side for instant feedback.
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeUserCode(input) {
|
|
61
|
+
if (typeof input !== "string")
|
|
62
|
+
return null;
|
|
63
|
+
const raw = input.toUpperCase().replace(/[\s-]/g, "");
|
|
64
|
+
if (!/^[A-Z0-9]{8}$/.test(raw))
|
|
65
|
+
return null;
|
|
66
|
+
return `${raw.slice(0, 4)}-${raw.slice(4)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The RFC 8628 §3.5 poll-loop step: what interval to poll with next, and whether to keep going.
|
|
70
|
+
* - authorization_pending → keep polling at the current interval
|
|
71
|
+
* - slow_down → keep polling, interval + 5s (the RFC-mandated increment)
|
|
72
|
+
* - anything else → stop (expired_token/access_denied = terminal; invalid_* = client bug)
|
|
73
|
+
*/
|
|
74
|
+
export function nextPollInterval(currentIntervalSeconds, error) {
|
|
75
|
+
if (error === "authorization_pending")
|
|
76
|
+
return { continue: true, intervalSeconds: currentIntervalSeconds };
|
|
77
|
+
if (error === "slow_down")
|
|
78
|
+
return { continue: true, intervalSeconds: currentIntervalSeconds + 5 };
|
|
79
|
+
return { continue: false, intervalSeconds: currentIntervalSeconds };
|
|
80
|
+
}
|
|
81
|
+
// ── scopes: endpoint paths + claim(api/scopes.ts 原件)─────────────────────────────────────────────────
|
|
82
|
+
export const SCOPES_V1_PATHS = {
|
|
83
|
+
/** GET (list) + POST (create). */
|
|
84
|
+
scopes: "/api/v1/scopes",
|
|
85
|
+
/** POST — mint a new access token bound to another scope. */
|
|
86
|
+
authScope: "/api/v1/auth/scope",
|
|
87
|
+
};
|
|
88
|
+
/** The JWT claim carrying the token's active scope. OPTIONAL — absent means GLOBAL_SCOPE (tokenScopeOf). */
|
|
89
|
+
export const SCOPE_CLAIM = "scope";
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConfigReader } from "./reader.js";
|
|
2
|
+
import { type DomainName } from "./types.js";
|
|
3
|
+
export type ConfigBundle = Partial<Record<DomainName, unknown>>;
|
|
4
|
+
/** Read the (portable, by default) domains out of any ConfigReader → a bundle object. Absent domains are
|
|
5
|
+
* omitted (not defaulted) so a re-import doesn't fabricate domains the source never set. */
|
|
6
|
+
export declare function exportBundle(reader: ConfigReader, domains?: readonly DomainName[]): Promise<ConfigBundle>;
|
|
7
|
+
/** Write a bundle to `<root>/config.d/<domain>.json` — each file atomic + VALIDATED (writeDomainFile parses
|
|
8
|
+
* against the domain zod, so a malformed source domain fails loudly rather than writing garbage). Returns the
|
|
9
|
+
* domains written. (The TOC owns `<root>`; secrets stay in `<root>/.env`, never in these files.) */
|
|
10
|
+
export declare function writeBundle(root: string, bundle: ConfigBundle): Promise<DomainName[]>;
|
|
11
|
+
/** Read a `<root>/config.d/*.json` bundle → raw domain configs (missing/empty file → omitted). The reverse of
|
|
12
|
+
* writeBundle: feed into sema-registry's per-domain PUT to import, or into the resolver to run locally. */
|
|
13
|
+
export declare function readBundle(root: string, domains?: readonly DomainName[]): Promise<ConfigBundle>;
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip between a config SOURCE (any {@link ConfigReader} — sema-registry's store OR a FileConfigStore)
|
|
3
|
+
* and a local `config.d/<domain>.json` BUNDLE. This is how a FLEET config seeds a TOC desktop (export → copy
|
|
4
|
+
* the bundle + set .env → run locally) and how a local bundle seeds sema-registry (readBundle → PUT each domain
|
|
5
|
+
* via the existing domain API). The bundle = the RAW authorable domain configs; `skills` carry their full
|
|
6
|
+
* bodies (SkillSpec.content is stored in full — NOT the /effective manifest), so there is no separate
|
|
7
|
+
* body-bundling step and the round-trip is symmetric.
|
|
8
|
+
*
|
|
9
|
+
* Node-only (writeBundle/readBundle touch the filesystem) → imported via `@sema-agent/settings-schema/node`.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { writeDomainFile } from "./file-edit.js";
|
|
14
|
+
import { PORTABLE_DOMAINS } from "./file-store.js";
|
|
15
|
+
import { isDomainName } from "./types.js";
|
|
16
|
+
/** Read the (portable, by default) domains out of any ConfigReader → a bundle object. Absent domains are
|
|
17
|
+
* omitted (not defaulted) so a re-import doesn't fabricate domains the source never set. */
|
|
18
|
+
export async function exportBundle(reader, domains = PORTABLE_DOMAINS) {
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const d of domains) {
|
|
21
|
+
const v = await reader.getDomain(d);
|
|
22
|
+
if (v !== undefined)
|
|
23
|
+
out[d] = v;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Write a bundle to `<root>/config.d/<domain>.json` — each file atomic + VALIDATED (writeDomainFile parses
|
|
28
|
+
* against the domain zod, so a malformed source domain fails loudly rather than writing garbage). Returns the
|
|
29
|
+
* domains written. (The TOC owns `<root>`; secrets stay in `<root>/.env`, never in these files.) */
|
|
30
|
+
export async function writeBundle(root, bundle) {
|
|
31
|
+
const written = [];
|
|
32
|
+
for (const [d, v] of Object.entries(bundle)) {
|
|
33
|
+
if (v === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
await writeDomainFile(root, d, v);
|
|
36
|
+
written.push(d);
|
|
37
|
+
}
|
|
38
|
+
return written;
|
|
39
|
+
}
|
|
40
|
+
/** Read a `<root>/config.d/*.json` bundle → raw domain configs (missing/empty file → omitted). The reverse of
|
|
41
|
+
* writeBundle: feed into sema-registry's per-domain PUT to import, or into the resolver to run locally. */
|
|
42
|
+
export async function readBundle(root, domains = PORTABLE_DOMAINS) {
|
|
43
|
+
const out = {};
|
|
44
|
+
await Promise.all(domains.map(async (d) => {
|
|
45
|
+
if (!isDomainName(d))
|
|
46
|
+
throw new Error(`"${d}" is not a config domain — refusing to build a config.d path from it (no traversal)`);
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = await readFile(path.join(root, "config.d", `${d}.json`), "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e.code === "ENOENT")
|
|
53
|
+
return; // absent file → omit (not fabricated)
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
const trimmed = text.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
out[d] = JSON.parse(trimmed);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(`invalid JSON in config.d/${d}.json`); // sanitized: never echo the raw file text
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure config functions + data shapes — LIFTED verbatim out of sema-registry's `store.ts` (the half that
|
|
3
|
+
* depends only on `./types`, zero app/DB/next coupling). sema-registry re-exports these so its existing
|
|
4
|
+
* `@/lib/config` importers keep working unchanged (ONE copy of the code, here).
|
|
5
|
+
*
|
|
6
|
+
* Two DETERMINISM refactors vs. the original store.ts (so the package is wall-clock-free and a local file
|
|
7
|
+
* source can produce byte-identical effective config for identical inputs):
|
|
8
|
+
* 1. `buildEffective(version, getRaw, updatedAt)` — `updatedAt` is now a REQUIRED param (was a
|
|
9
|
+
* `new Date().toISOString()` default). Every caller supplies it explicitly.
|
|
10
|
+
* 2. `emptyEffective()` seeds the epoch (`new Date(0).toISOString()`) instead of the wall clock, matching
|
|
11
|
+
* the stores' existing "epoch sentinel" reduce seed.
|
|
12
|
+
*
|
|
13
|
+
* The fat `ConfigStore` interface itself STAYS in sema-registry (app-only, ~32 publish/RBAC/worker-status
|
|
14
|
+
* methods); only the narrow {@link ConfigReader} (reader.ts) is carried here.
|
|
15
|
+
*/
|
|
16
|
+
import type { z } from "zod";
|
|
17
|
+
import { type DomainConfig, type DomainName, type EffectiveConfig, type EffectiveWire } from "./types.js";
|
|
18
|
+
/** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
|
|
19
|
+
* `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
|
|
20
|
+
* space-spanning auth credentials (`Bearer <token>`) FIRST (the token pass can't see across the space), then
|
|
21
|
+
* any secret-shaped run (`%` kept in the run so a percent-encoded secret stays whole for the percent-aware
|
|
22
|
+
* containsSecretToken). invariant ①: a secret VALUE never rides an error. R13/R14. */
|
|
23
|
+
export declare function redactSecrets(text: string): string;
|
|
24
|
+
/** Sanitize a ZodError IN PLACE so no surfaced error (message / `received` / nested `unionErrors`) echoes a
|
|
25
|
+
* pasted secret. RECURSES into `unionErrors` (a `z.union` failure nests a ZodError per member — e.g. RoleTarget
|
|
26
|
+
* `{model}|{select}`) and redacts string path segments too. The ONE sanitizer every validation entry point
|
|
27
|
+
* uses (parseDomain / validateDomain / validateRemoteExec / write*). R14. */
|
|
28
|
+
export declare function sanitizeZodError(err: z.ZodError): z.ZodError;
|
|
29
|
+
/** Result of a CAS write (sema-registry's `ConfigStore.setDomainsIfUnchanged`): applied (new `version`) or
|
|
30
|
+
* rejected because the version moved (`currentVersion` = what it is now, so the caller can re-read + retry). */
|
|
31
|
+
export type CasWriteResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
version: number;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
currentVersion: number;
|
|
37
|
+
};
|
|
38
|
+
/** sema-registry's lifecycle bookkeeping for one worker (its OWN clock — design/25 决策3). */
|
|
39
|
+
export interface WorkerLifecycleRecord {
|
|
40
|
+
name: string;
|
|
41
|
+
/** when sema-registry FIRST saw this worker — TTL is measured from here. */
|
|
42
|
+
firstSeenAt: string;
|
|
43
|
+
/** last time the reverse-proxy saw traffic to it (idle detection). */
|
|
44
|
+
lastActiveAt?: string;
|
|
45
|
+
}
|
|
46
|
+
/** RBAC roles, hierarchical: viewer < editor < publisher < admin. */
|
|
47
|
+
export type UserRole = "viewer" | "editor" | "publisher" | "admin";
|
|
48
|
+
export declare const ROLE_RANK: Record<UserRole, number>;
|
|
49
|
+
export declare const USER_ROLES: readonly UserRole[];
|
|
50
|
+
/** True if `role` is at least `min` in the hierarchy (e.g. publisher satisfies editor). */
|
|
51
|
+
export declare function roleAtLeast(role: UserRole, min: UserRole): boolean;
|
|
52
|
+
export declare function isRole(v: unknown): v is UserRole;
|
|
53
|
+
/**
|
|
54
|
+
* Minimum role to WRITE a domain. Default = `editor`; `skills` and `mcp` are raised to `publisher`
|
|
55
|
+
* because their content goes straight into the agent's system prompt / tool surface — one edit retargets
|
|
56
|
+
* the prompt of every worker × scenario that loads it (docs/MCP-SKILLS.md §3.3, decision §6 Q3 从严).
|
|
57
|
+
* Looked up by the [domain] PUT route. Any domain absent here uses {@link DEFAULT_DOMAIN_WRITE_ROLE}.
|
|
58
|
+
*/
|
|
59
|
+
export declare const DEFAULT_DOMAIN_WRITE_ROLE: UserRole;
|
|
60
|
+
export declare const DOMAIN_WRITE_ROLE: Partial<Record<DomainName, UserRole>>;
|
|
61
|
+
/** The minimum role required to write `domain` (the raised gate for skills/mcp, else editor). */
|
|
62
|
+
export declare function domainWriteRole(domain: DomainName): UserRole;
|
|
63
|
+
/** A console user (no secret). */
|
|
64
|
+
export interface User {
|
|
65
|
+
username: string;
|
|
66
|
+
role: UserRole;
|
|
67
|
+
/** True = an SSO user (empty password hash) auto-provisioned from Gitea OAuth — signs in via Gitea only,
|
|
68
|
+
* no password. Derived on list (we expose the boolean, never the hash) so /users can distinguish them. */
|
|
69
|
+
sso?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** A user with its password hash (login only — never sent to the client). */
|
|
72
|
+
export interface UserAuth extends User {
|
|
73
|
+
passwordHash: string;
|
|
74
|
+
/** scrypt hash of the user's API token (NOT the plaintext), or undefined/"" if none. A `Bearer cc_<user>_<rand>`
|
|
75
|
+
* is authenticated by scrypt-verifying `<rand>` against this. Set/revoked via `ConfigStore.setUserApiToken`. */
|
|
76
|
+
apiTokenHash?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Outcome of an atomic `ConfigStore.approvePublish`. */
|
|
79
|
+
export type ApprovePublishResult = {
|
|
80
|
+
status: "no-pending";
|
|
81
|
+
} | {
|
|
82
|
+
status: "is-requester";
|
|
83
|
+
} | {
|
|
84
|
+
status: "approved";
|
|
85
|
+
approvals: number;
|
|
86
|
+
required: number;
|
|
87
|
+
} | {
|
|
88
|
+
status: "published";
|
|
89
|
+
approvals: number;
|
|
90
|
+
version: number;
|
|
91
|
+
}
|
|
92
|
+
/** The draft changed since the request was opened — publishing it would release content the approvers
|
|
93
|
+
* never reviewed. The pending is cleared; re-request against the current draft. */
|
|
94
|
+
| {
|
|
95
|
+
status: "draft-moved";
|
|
96
|
+
expectedVersion: number;
|
|
97
|
+
currentVersion: number;
|
|
98
|
+
};
|
|
99
|
+
/** A pending publish awaiting approvals (publish approval workflow). Singleton — the current draft. */
|
|
100
|
+
export interface PendingPublish {
|
|
101
|
+
requestedBy: string;
|
|
102
|
+
requestedAt: string;
|
|
103
|
+
summary?: string;
|
|
104
|
+
/** The config version this request asks to release — approvals sign off on EXACTLY this draft; the final
|
|
105
|
+
* approval refuses (draft-moved) if the version has advanced since. Optional only for legacy rows
|
|
106
|
+
* persisted before the field existed (treated as moved — never published). */
|
|
107
|
+
draftVersion?: number;
|
|
108
|
+
/** approvers who've signed off (deduped; the requester may not approve their own). */
|
|
109
|
+
approvals: {
|
|
110
|
+
user: string;
|
|
111
|
+
at: string;
|
|
112
|
+
}[];
|
|
113
|
+
}
|
|
114
|
+
/** A frozen, deliberately-published config release (draft→publish gate, E). */
|
|
115
|
+
export interface PublishedSnapshot {
|
|
116
|
+
/** the frozen merged config consumers receive in publish mode (carries the version at publish time). */
|
|
117
|
+
config: EffectiveConfig;
|
|
118
|
+
/** when it was promoted to published. */
|
|
119
|
+
publishedAt: string;
|
|
120
|
+
/** who published it. */
|
|
121
|
+
publishedBy: string;
|
|
122
|
+
/** optional release note. */
|
|
123
|
+
summary?: string;
|
|
124
|
+
/** MONOTONIC publish counter — increments on EVERY publish INCLUDING a rollback-republish, so it never goes
|
|
125
|
+
* backward (unlike `config.version`, which a rollback re-freezes to an OLDER value). The sole ordering/
|
|
126
|
+
* freshness authority consumers compare across published snapshots (design CENTER-CONTROL-PLANE R2-S #9 /
|
|
127
|
+
* R2-D4). `config.version` stays the CAS/contentHash anchor WITHIN a revision; revision orders ACROSS them. */
|
|
128
|
+
globalRevision: number;
|
|
129
|
+
}
|
|
130
|
+
/** The empty/defaults effective config at version 0 — what publish mode serves before the first publish.
|
|
131
|
+
* Seeds the epoch (`new Date(0)`) so it's deterministic (the wall-clock default was the original store.ts
|
|
132
|
+
* non-determinism — see the file header). */
|
|
133
|
+
export declare function emptyEffective(): EffectiveConfig;
|
|
134
|
+
/** Validate + fill defaults for a domain payload (or the schema's empty default when absent/invalid). */
|
|
135
|
+
export declare function parseDomain<K extends DomainName>(domain: K, raw: unknown): DomainConfig[K];
|
|
136
|
+
/**
|
|
137
|
+
* Shared GC predicate for worker-status rows (F10) — keeps memory/sqlite/tidb identical. Given the rows'
|
|
138
|
+
* `{name, updatedAt}` and the GC options, returns the names to prune (A: not in `keepNames`; B: older than
|
|
139
|
+
* `ttlMs`). Pass neither selector → prunes nothing (guards against a wipe-all). See `ConfigStore.gcWorkerStatuses`.
|
|
140
|
+
*/
|
|
141
|
+
export declare function statusRowsToPrune(rows: {
|
|
142
|
+
name: string;
|
|
143
|
+
updatedAt: string;
|
|
144
|
+
}[], opts: {
|
|
145
|
+
keepNames?: string[];
|
|
146
|
+
ttlMs?: number;
|
|
147
|
+
now?: Date;
|
|
148
|
+
}): string[];
|
|
149
|
+
/**
|
|
150
|
+
* Merge every domain into the effective config a consumer pulls. Shared by all store impls.
|
|
151
|
+
* `updatedAt` is REQUIRED (the determinism refactor — the original store.ts defaulted it to the wall clock,
|
|
152
|
+
* which made `emptyEffective`/the memory store non-deterministic). Every caller now supplies it explicitly.
|
|
153
|
+
*/
|
|
154
|
+
/** Lift the governance trio out of a LEGACY runtime doc (pre-0.6 stored runtime carried autonomy/commandPolicy/
|
|
155
|
+
* approvalRequire). Used as the governance domain's read fallback when governance was NEVER set — so a 0.5
|
|
156
|
+
* store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
|
|
157
|
+
* (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime"). */
|
|
158
|
+
export declare function legacyGovernanceFromRuntime(rawRuntime: unknown): unknown;
|
|
159
|
+
/** A warning emitted by {@link buildEffective} when it clamps/degrades instead of throwing (0.8.1), or by
|
|
160
|
+
* {@link FileConfigStore} when a file it will never read sits in `config.d/` (0.19.0). Callers route it to
|
|
161
|
+
* their audit stream; omitting the callback keeps the resilience but drops the telemetry.
|
|
162
|
+
*
|
|
163
|
+
* 0.19.0 加了两个 kind(additive union member —— 用 `switch` 穷举本类型的消费方需补臂):
|
|
164
|
+
* `"unknown-keys-dropped"` / `"unread-config-file"`,成案见 {@link droppedKeys}(sema-server
|
|
165
|
+
* #322:配置写了、进程跑着、值没到,而没有任何一个字说出来)。 */
|
|
166
|
+
export type EffectiveReadWarning = {
|
|
167
|
+
domain: "hosts";
|
|
168
|
+
kind: "hosts-grandfathered";
|
|
169
|
+
droppedNames: string[];
|
|
170
|
+
truncatedFrom?: number;
|
|
171
|
+
} | {
|
|
172
|
+
domain: DomainName;
|
|
173
|
+
kind: "domain-defaulted";
|
|
174
|
+
error: unknown;
|
|
175
|
+
}
|
|
176
|
+
/** 域文档里那些**不属于该域 schema** 的键:行为照旧(zod 非 strict = 剥掉,前向兼容不动),但点名
|
|
177
|
+
* 说出来。`keys` = 点号路径(`infraCostRates.toolCalMicroUsd` / `commandPolicy.0.typo`),见
|
|
178
|
+
* {@link droppedKeys} 的扫描边界。 */
|
|
179
|
+
| {
|
|
180
|
+
domain: DomainName;
|
|
181
|
+
kind: "unknown-keys-dropped";
|
|
182
|
+
keys: string[];
|
|
183
|
+
}
|
|
184
|
+
/** open-world 域(`.passthrough()`)里**已送达但本包不认识**的键:值原样到了消费方手里(承运),
|
|
185
|
+
* 但拼错的键名同样该有人说 —— 认不认归持有键表的消费方,`keys` 是点号路径。 */
|
|
186
|
+
| {
|
|
187
|
+
domain: DomainName;
|
|
188
|
+
kind: "unknown-keys-carried";
|
|
189
|
+
keys: string[];
|
|
190
|
+
}
|
|
191
|
+
/** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
|
|
192
|
+
| {
|
|
193
|
+
kind: "unread-config-file";
|
|
194
|
+
file: string;
|
|
195
|
+
domain?: DomainName;
|
|
196
|
+
why: "not-a-domain" | "non-portable-domain";
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),点号路径形。
|
|
200
|
+
*
|
|
201
|
+
* 成案(sema-server #322 真事故):往 `runtime` 文档里加一个不属于该域的键,两条腿都会把它剥掉,
|
|
202
|
+
* `domainErrors` 空、解析"成功"——配置写了、值没到、零告警。剥除行为本身是**刻意的前向兼容**
|
|
203
|
+
* (新 center 的新键不该炸老消费方),所以修法不是收紧成 `.strict()`,而是让它**响亮**:
|
|
204
|
+
* {@link buildEffective} 每域解析成功后调用本函数,非空即发 `"unknown-keys-dropped"` 警告。
|
|
205
|
+
*
|
|
206
|
+
* **嵌套也扫**(codex 复审 F2 真 finding):`{infraCostRates:{toolCalMicroUsd:10}}` 这种**拼错的嵌套
|
|
207
|
+
* 键**在顶层看不见(`infraCostRates` 键还在),只看顶层等于对计费面的错键完全失明。故按 raw↔parsed
|
|
208
|
+
* 的键集逐层对比(对象逐键、数组同长时逐下标),不依赖任何 zod 内省。
|
|
209
|
+
*
|
|
210
|
+
* `runtime` 域的治理三件({@link GOVERNANCE_MIRROR_KEYS})**不算被丢**:0.5 期 runtime 文档携带它们是
|
|
211
|
+
* 成文的抬升位(本文件的 governance 抬升函数会把它们抬进 governance 域),报出来是噪声。豁免只在顶层。
|
|
212
|
+
*
|
|
213
|
+
* 边界(如实登记):① 深度上限 {@link DROP_SCAN_MAX_DEPTH}、键数上限 {@link DROP_SCAN_MAX_KEYS}(告警
|
|
214
|
+
* 载荷有界,超出即少报);② 数组长度不同就整段跳过(元素级裁剪由各自的 clamp 告警负责);③ 只比对纯
|
|
215
|
+
* JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包 try/catch 且**绝不**
|
|
216
|
+
* 让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作"没看见",不影响解析。
|
|
217
|
+
*/
|
|
218
|
+
export declare function droppedKeys(domain: DomainName, raw: unknown, parsed: unknown): string[];
|
|
219
|
+
/**
|
|
220
|
+
* 一份已解析的 open-world 域文档里,**本包不认识但已承运**的键(点号路径)。非 open-world 域恒空
|
|
221
|
+
* (它们的未知键走 {@link droppedKeys} 那条更重的「被丢了」告警)。
|
|
222
|
+
*
|
|
223
|
+
* 边界(如实):只看 {@link OPEN_WORLD_KNOWN_KEYS} 登记了的层级 —— 未登记层级(如某个未知键自己的
|
|
224
|
+
* 子树)不再下探,那本就是消费方自定义的内容,逐层猜没有意义。
|
|
225
|
+
*/
|
|
226
|
+
export declare function carriedUnknownKeys(domain: DomainName, parsed: unknown): string[];
|
|
227
|
+
/**
|
|
228
|
+
* 一域的 parse + **静默剥键/静默承运两条告警**(0.19.0)。`buildEffective` 与 `FileConfigStore.getDomain`
|
|
229
|
+
* 共用这一个判据 —— 此前只有 effective 那条路径有门,而 `getDomain` 是 `exportBundle` 的读法
|
|
230
|
+
* (codex 复审 round2 F3:导出一份被剥了键的域再导入 = 持久的配置丢失,而且全程无声)。
|
|
231
|
+
*/
|
|
232
|
+
export declare function parseDomainLoud<K extends DomainName>(domain: K, raw: unknown, onWarning?: (w: EffectiveReadWarning) => void): DomainConfig[K];
|
|
233
|
+
/** Per-domain READ-corruption policy (0.8.1) — what buildEffective does when a stored domain no longer parses.
|
|
234
|
+
* `"default"` = degrade to the schema default + warn (safe: the default only REMOVES capability, fail-closed).
|
|
235
|
+
* `"throw"` = keep fail-loud (defaulting would LOOSEN gates or destructively apply — worse than an outage,
|
|
236
|
+
* consumers fail-static on last-good). A `satisfies Record<DomainName,…>` forces every FUTURE domain to make
|
|
237
|
+
* this call explicitly (compile error until it's classified). Per-domain rationale:
|
|
238
|
+
* - models/skills/mcp/plugins/scenarios/systems/collab: additive capability catalogs — empty = the
|
|
239
|
+
* capability disappears (fail-closed), no gate is lost.
|
|
240
|
+
* - hosts: empty registry = no auto-placement targets (availability-only); self-registration re-lands
|
|
241
|
+
* entries as approved:false = 待批 (fail-closed). Plus the precise grandfather clamp runs FIRST.
|
|
242
|
+
* - rosters: THROW — a lost roster makes resolveEffectiveForWorker fall back to the FULL models catalog
|
|
243
|
+
* ("global(fallback)"), silently WIDENING every restricted worker's model access. 放权,不许.
|
|
244
|
+
* - workers: THROW — an empty fleet spec would make the reconciler RETIRE every managed worker
|
|
245
|
+
* (destructive apply — far worse than a read outage).
|
|
246
|
+
* - runtime: THROW — defaulting drops centrally-published rate/cost ceilings = unbounded spend (fail-open
|
|
247
|
+
* on the budget axis).
|
|
248
|
+
* - governance: THROW — defaulting drops autonomy/commandPolicy/approvalRequire = the consumer falls back
|
|
249
|
+
* to env/defaults with the operator's guardrails silently gone. 放权,不许.
|
|
250
|
+
* - entitlement: THROW — defaulting drops the org kill-switch/budget ceilings/tier restrictions for every
|
|
251
|
+
* principal. 放权,不许.
|
|
252
|
+
* - execution: THROW — defaulting flips required:true (sandbox mandate) back to false. 放权,不许.
|
|
253
|
+
* (0.10.1: sessionMirror rides the SAME stance — defaulting would evaporate an org's required:true
|
|
254
|
+
* mirror mandate = 审计面静默放权; the shared rationale is exactly WHY it was收编 here, not a new domain.)
|
|
255
|
+
* - projects: THROW — defaulting evaporates the whole identity ledger: once the S4 worker gate ships,
|
|
256
|
+
* every `proj:*` derivation fails-fast (全租户记忆面停摆 — consumers fail-static on last-good is
|
|
257
|
+
* strictly better); WORSE, the claim write face is read-modify-write — reading a defaulted-empty
|
|
258
|
+
* ledger and writing it back would CLOBBER existing registrations down to one entry (data loss,
|
|
259
|
+
* one layer beyond the rosters "放权" rationale). */
|
|
260
|
+
export declare const DOMAIN_READ_FALLBACK: {
|
|
261
|
+
readonly models: "default";
|
|
262
|
+
readonly rosters: "throw";
|
|
263
|
+
readonly skills: "default";
|
|
264
|
+
readonly mcp: "default";
|
|
265
|
+
readonly plugins: "default";
|
|
266
|
+
readonly scenarios: "default";
|
|
267
|
+
readonly systems: "default";
|
|
268
|
+
readonly collab: "default";
|
|
269
|
+
readonly workers: "throw";
|
|
270
|
+
readonly hosts: "default";
|
|
271
|
+
readonly runtime: "throw";
|
|
272
|
+
readonly governance: "throw";
|
|
273
|
+
readonly entitlement: "throw";
|
|
274
|
+
readonly execution: "throw";
|
|
275
|
+
readonly projects: "throw";
|
|
276
|
+
readonly prompts: "default";
|
|
277
|
+
readonly limits: "throw";
|
|
278
|
+
};
|
|
279
|
+
/** Result of {@link grandfatherHostsRead}. `raw` is the (possibly clamped) document to parse; when NOTHING
|
|
280
|
+
* was clamped it is the ORIGINAL reference — well-formed data stays byte-identical through the read path. */
|
|
281
|
+
export interface GrandfatherHostsReadResult {
|
|
282
|
+
raw: unknown;
|
|
283
|
+
/** Host names DROPPED because they exceed HOST_NAME_MAX (63) — truncating would forge a NEW identity that
|
|
284
|
+
* matches no ORCH_HOST_ID (and could collide into the duplicate-name refine), so the entry is skipped whole. */
|
|
285
|
+
droppedNames: string[];
|
|
286
|
+
/** Set when the hosts array (after drops) exceeded HOSTS_ARRAY_MAX (1024) and was sliced — the pre-slice length. */
|
|
287
|
+
truncatedFrom?: number;
|
|
288
|
+
}
|
|
289
|
+
/** READ-path grandfather clamp for the 0.8.0 hosts tightening (0.8.1, S3 M-1). Clamps ONLY the two bounds
|
|
290
|
+
* 0.8.0 added (name > 63 → drop the entry; array > 1024 → slice) so 0.7-era legal stored data keeps reading;
|
|
291
|
+
* everything else (regex, types, duplicates) is left for parseDomain — this is a targeted grandfather, not a
|
|
292
|
+
* lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds. */
|
|
293
|
+
export declare function grandfatherHostsRead(raw: unknown): GrandfatherHostsReadResult;
|
|
294
|
+
export declare function buildEffective(version: number, getRaw: (d: DomainName) => unknown, updatedAt: string, onWarning?: (w: EffectiveReadWarning) => void): EffectiveConfig;
|
|
295
|
+
/** READ-side tolerant judgment for a GET /effective **wire payload**(the {@link EffectiveWire} contract)at
|
|
296
|
+
* its CONSUMPTION boundary — a service pulling from a remote center passes the parsed JSON here instead of
|
|
297
|
+
* type-asserting it(0.12.0,[2281] 裁B:「两个契约共用一个拼写不是一个被检查的契约」——拼写副本要在
|
|
298
|
+
* 消费端被真校验)。ONE judgment, two legs: every known domain goes through the SAME `parseDomain` +
|
|
299
|
+
* {@link DOMAIN_READ_FALLBACK} policy as {@link buildEffective}(catalog 域坏形 → schema default + warning;
|
|
300
|
+
* gate 域坏形 → throw,caller 的整包 catch 决定回落),including the governance legacy-lift and the hosts
|
|
301
|
+
* read-grandfather — DELEGATED to buildEffective, not re-implemented(单判据双腿消费,§M4)。
|
|
302
|
+
*
|
|
303
|
+
* `skills` is the ONE wire divergence: on the wire it is a MANIFEST(content → contentHash,
|
|
304
|
+
* {@link SkillsManifest}),so the full skills-domain schema would reject every LEGAL manifest and silently
|
|
305
|
+
* default the domain(蒸发整张 skill 清单)— it validates against {@link SkillsManifestSchema} instead,
|
|
306
|
+
* same "default" fallback tier as the skills domain itself.
|
|
307
|
+
*
|
|
308
|
+
* OPEN-WORLD on unknown top-level keys: they ride through VERBATIM(never validated, never dropped)— a
|
|
309
|
+
* newer center's new domain reaches an older service untouched(§M2 能力面语义:读不到即走老路,不该炸),
|
|
310
|
+
* and the legacy `teams` key(not a domain since 0.7, services still consume it for backward compat)keeps
|
|
311
|
+
* flowing. Throws ONLY on garbage no caller can partially trust: a non-object payload, or a missing/
|
|
312
|
+
* non-finite `version` — that is not "a bad domain", it is not an effective config at all. */
|
|
313
|
+
export declare function readEffectiveWire(raw: unknown, onWarning?: (w: EffectiveReadWarning) => void): EffectiveWire;
|
|
314
|
+
/** 档位组→引擎单表的唯一解析点(0.9.0)。返回 ACTIVE 档位组的绑定表(剔除未绑档),即 core
|
|
315
|
+
* `RunnerDeps.tiers` 要吃的那张单表——组切换=center 换这张表下发,零新引擎机制。无 active 组/未命中/
|
|
316
|
+
* 组内一档未绑 → `undefined`(引擎档位层恒惰性,与「未配 tiers」逐字同义)。消费方(center /effective
|
|
317
|
+
* 投影、service applyEffective 接线)都走这一个函数,不要各自解析。 */
|
|
318
|
+
export declare function resolveActiveTiers(models: Pick<DomainConfig["models"], "tierGroups" | "activeTierGroup">): Record<string, string> | undefined;
|