@sema-agent/client-core 0.3.0 → 0.4.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 +23 -1
- package/dist/agentsWireCaps.d.ts +112 -0
- package/dist/agentsWireCaps.js +319 -0
- package/dist/attachmentsWireCaps.d.ts +47 -0
- package/dist/attachmentsWireCaps.js +46 -0
- package/dist/classifierVerdictWire.d.ts +45 -0
- package/dist/classifierVerdictWire.js +76 -0
- package/dist/clientContextWireCaps.d.ts +46 -0
- package/dist/clientContextWireCaps.js +48 -0
- package/dist/cloudConfigWireCaps.d.ts +110 -0
- package/dist/cloudConfigWireCaps.js +228 -0
- package/dist/controlRouter.d.ts +191 -0
- package/dist/controlRouter.js +244 -0
- package/dist/effortWire.d.ts +34 -0
- package/dist/effortWire.js +38 -0
- package/dist/engineWireSdk.d.ts +49 -0
- package/dist/engineWireSdk.js +61 -0
- package/dist/forkWireCaps.d.ts +25 -0
- package/dist/forkWireCaps.js +42 -0
- package/dist/hostEnv.d.ts +16 -0
- package/dist/hostEnv.js +17 -0
- package/dist/imagesWireCaps.d.ts +28 -0
- package/dist/imagesWireCaps.js +49 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +41 -0
- package/dist/liveModelCatalog.d.ts +62 -0
- package/dist/liveModelCatalog.js +79 -0
- package/dist/mcpWireCaps.d.ts +41 -0
- package/dist/mcpWireCaps.js +51 -0
- package/dist/modelBudgetRule.d.ts +44 -0
- package/dist/modelBudgetRule.js +73 -0
- package/dist/modelWireCaps.d.ts +17 -0
- package/dist/modelWireCaps.js +16 -0
- package/dist/notifications.d.ts +150 -4
- package/dist/notifications.js +472 -0
- package/dist/permissionWireCaps.d.ts +37 -0
- package/dist/permissionWireCaps.js +46 -0
- package/dist/promptProfileWireCaps.d.ts +17 -0
- package/dist/promptProfileWireCaps.js +20 -0
- package/dist/retainBackgroundWireCaps.d.ts +50 -0
- package/dist/retainBackgroundWireCaps.js +58 -0
- package/dist/rewindWireCaps.d.ts +36 -0
- package/dist/rewindWireCaps.js +41 -0
- package/dist/selfOrchestrationWireCaps.d.ts +40 -0
- package/dist/selfOrchestrationWireCaps.js +48 -0
- package/dist/sessionModelLatch.d.ts +35 -0
- package/dist/sessionModelLatch.js +63 -0
- package/dist/skillsWireCaps.d.ts +47 -0
- package/dist/skillsWireCaps.js +41 -0
- package/dist/sseIdleTriage.d.ts +94 -0
- package/dist/sseIdleTriage.js +142 -0
- package/dist/ultracodeWireCaps.d.ts +90 -0
- package/dist/ultracodeWireCaps.js +159 -0
- package/dist/webSearchWireCaps.d.ts +54 -0
- package/dist/webSearchWireCaps.js +76 -0
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/classifierVerdictWire.ts 的
|
|
3
|
+
* **判定半场**搬入(帧识别 + 文案),`surfaceClassifierDeny()` 的**执行半场**留宿主
|
|
4
|
+
* (它写 Ink `AppState.notifications` 并 lazy `require('../utils/autoModeDenials.js')`——
|
|
5
|
+
* 两者都是 TUI/Node 私有面,进不了端无关包)。
|
|
6
|
+
* 🔴 本文件零模块级可变状态(只有常量),故不受 B1「写读口必须同实例」纪律约束:壳侧留
|
|
7
|
+
* 执行半场、判定走本包,是安全的拆缝(B6 T35/T36/T44 同款「判定半场搬入」姿势)。
|
|
8
|
+
* 逐字纪律:CLASSIFIER_DENY_SIGNATURE / NOTICE_REASON_MAX(80)/ 通知行三段格式一字节不改。
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* src/sema/classifierVerdictWire.ts — auto-mode 分类器裁决帧的壳侧消费(分类器批3 壳半场,黑板 [907])。
|
|
12
|
+
*
|
|
13
|
+
* WIRE 形状(gap-check 2026-07-16 实勘,core 1.279 dist 亲核):
|
|
14
|
+
* 引擎侧分类器 block(core hooks.js runToolGate)→ decision = deny + decisionReason:"classifier",
|
|
15
|
+
* permissionDenied 钩子收 source:"classifier"(server 1.209 只作 metrics 计量,不出独立 deny 帧)——
|
|
16
|
+
* 壳能看见的唯一 wire 事实 = `tool_end` 帧:
|
|
17
|
+
* { type:'tool_end', isError:true, output: [{type:'text', text:'<system-reminder>\n
|
|
18
|
+
* auto-mode classifier blocked this call[: <reason>|: [<category>]]\n</system-reminder>'}] }
|
|
19
|
+
* (core hooks.js L138 deny message 逐字 + formatHookFeedback 包 system-reminder;
|
|
20
|
+
* agent-loop createErrorToolResult 落 content 数组;runtask toolOutputFrom 原样上 wire。)
|
|
21
|
+
* ⇒ 消费面按此签名结构性识别,等价于「deny 帧带 source:'classifier'」的判定(签名字符串是
|
|
22
|
+
* core 的机器产文面,非模型作文——模型 echo 不会以该签名做 tool_end 错误体开头)。
|
|
23
|
+
*
|
|
24
|
+
* 🔴 fail-soft 铁律:识别不中一律静默走原路,绝不伤害事件流。
|
|
25
|
+
* 🔴 UNTRUSTED:reason 为分类器(模型)产文,只渲染绝不回喂;通知行截断 ≤80(207 同款)。
|
|
26
|
+
*/
|
|
27
|
+
/** core hooks.js 分类器 deny message 的机器签名(逐字锚;lockstep:core 改文案这里必须跟)。 */
|
|
28
|
+
export const CLASSIFIER_DENY_SIGNATURE = 'auto-mode classifier blocked this call';
|
|
29
|
+
/** 通知 reason 段截断(CC 207 L696763 同款 80 → 79+…)。 */
|
|
30
|
+
const NOTICE_REASON_MAX = 80;
|
|
31
|
+
/** wire tool_end.output → 文本(string / content 块数组 / {content} 三形;识别用,轻量本地版)。 */
|
|
32
|
+
function textFromWireOutput(output) {
|
|
33
|
+
if (typeof output === 'string')
|
|
34
|
+
return output;
|
|
35
|
+
const blocks = Array.isArray(output)
|
|
36
|
+
? output
|
|
37
|
+
: output !== null && typeof output === 'object' && Array.isArray(output.content)
|
|
38
|
+
? (output.content)
|
|
39
|
+
: undefined;
|
|
40
|
+
if (!blocks)
|
|
41
|
+
return undefined;
|
|
42
|
+
const texts = blocks
|
|
43
|
+
.map(b => (b !== null && typeof b === 'object' && typeof b.text === 'string' ? b.text : undefined))
|
|
44
|
+
.filter((t) => t !== undefined);
|
|
45
|
+
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** 去 <system-reminder> 包裹(formatHookFeedback 逆变换;不包裹则原样)。 */
|
|
48
|
+
function unwrapSystemReminder(text) {
|
|
49
|
+
const m = /^\s*<system-reminder>\s*\n?([\s\S]*?)\n?\s*<\/system-reminder>\s*$/.exec(text);
|
|
50
|
+
return m ? m[1] : text;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 结构性识别一张 tool_end 帧是否为分类器 deny 裁决帧。
|
|
54
|
+
* 判定 = isError:true ∧ 错误体(去包裹后)以 core 机器签名开头。识别不中 ⇒ undefined(原路)。
|
|
55
|
+
*/
|
|
56
|
+
export function classifierDenyFromToolEnd(ev) {
|
|
57
|
+
if (!ev || ev.type !== 'tool_end' || ev.isError !== true)
|
|
58
|
+
return undefined;
|
|
59
|
+
const raw = textFromWireOutput(ev.output);
|
|
60
|
+
if (raw === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
const message = unwrapSystemReminder(raw).trim();
|
|
63
|
+
if (!message.startsWith(CLASSIFIER_DENY_SIGNATURE))
|
|
64
|
+
return undefined;
|
|
65
|
+
const tail = message.slice(CLASSIFIER_DENY_SIGNATURE.length);
|
|
66
|
+
const reason = tail.startsWith(': ') ? tail.slice(2).trim() : undefined;
|
|
67
|
+
return { message, ...(reason !== undefined && reason.length > 0 ? { reason } : {}) };
|
|
68
|
+
}
|
|
69
|
+
/** 通知行文案(exported for tests;CC 207 三段 text 化)。 */
|
|
70
|
+
export function classifierDenyNoticeText(toolName, reason) {
|
|
71
|
+
let r = reason ?? '';
|
|
72
|
+
if (r.length > NOTICE_REASON_MAX)
|
|
73
|
+
r = `${r.slice(0, NOTICE_REASON_MAX - 1)}…`;
|
|
74
|
+
const name = toolName.length > 0 ? toolName.toLowerCase() : 'tool';
|
|
75
|
+
return `${name} denied by auto mode${r ? ` · ${r}` : ''} · /permissions`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/clientContextWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/clientContextWireCaps.ts — PURE projection: the user's LOCAL identity facts (time zone + optional
|
|
6
|
+
* email) → the `TaskRequest.clientContext` object the SERVICE relays to core `TaskSpec.clientContext`.
|
|
7
|
+
*
|
|
8
|
+
* THE GAP THIS CLOSES (design/112, core/search AI 2026-06-30 — core 1.181): the worker container runs UTC and
|
|
9
|
+
* knows neither the user's time zone nor their identity. Without this, core renders the `# Environment` block's
|
|
10
|
+
* "today" in CONTAINER time (UTC) — a TOB-fleet user in Asia/Tokyo sees the wrong date, and the model has no
|
|
11
|
+
* email to attribute work. core 1.181 added the seam `TaskSpec.clientContext?: { timeZone?, userEmail? }`; the
|
|
12
|
+
* CLIENT half is the shell stamping the user's real IANA zone (+ email when known) onto every request, so core
|
|
13
|
+
* localizes "today" to the user's zone and ANNOTATES it (e.g. `(Asia/Tokyo)`); absent ⇒ core annotates `(UTC)`.
|
|
14
|
+
*
|
|
15
|
+
* SECURITY/CONTRACT (core 1.186 hardening, blackboard 2026-06-30):
|
|
16
|
+
* - The `locale` field was a DEAD (zero-consumer) field in 1.181 and was DELETED in 1.186 — we NEVER set it
|
|
17
|
+
* (only `timeZone` + `userEmail` actually drive rendering). An invalid IANA zone is core's problem to
|
|
18
|
+
* validate (1.186: a malformed zone falls back to UTC + `(UTC)` rather than lying); the shell just forwards
|
|
19
|
+
* what `Intl` resolved (always a valid zone on a real host).
|
|
20
|
+
* - `userEmail` is benign identity metadata, NOT a secret/token (TaskRequest secret discipline still holds —
|
|
21
|
+
* no tokens cross here). Sourced from `SEMA_USER_EMAIL` only when the shell actually knows it; absent ⇒ omit.
|
|
22
|
+
*
|
|
23
|
+
* PURE — no module-scope IO of its OWN beyond the caller-supplied `timeZone` + `env`, so it bundles + unit-tests
|
|
24
|
+
* without the heavy settings/Intl graph (the caller passes the cached `getTimeZone()` from utils/intl). Mirrors
|
|
25
|
+
* the other *WireCaps projection modules (selfOrchestration/webSearch).
|
|
26
|
+
*/
|
|
27
|
+
import { type EnvLike } from './hostEnv.js';
|
|
28
|
+
/** The env key the shell reads for the user's email (optional). SEMA_-namespaced. */
|
|
29
|
+
export declare const USER_EMAIL_ENV: "SEMA_USER_EMAIL";
|
|
30
|
+
/** The `clientContext` shape the SDK exposes (core 1.186: ONLY timeZone + userEmail are live fields). */
|
|
31
|
+
export interface ClientContext {
|
|
32
|
+
timeZone?: string;
|
|
33
|
+
userEmail?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the `{ clientContext }` partial for `toTaskRequest` to spread.
|
|
37
|
+
* - `timeZone`: the caller's resolved IANA zone (utils/intl `getTimeZone()` — always a valid zone on a host).
|
|
38
|
+
* Omitted only if the host has no resolvable zone (degenerate stripped-ICU) so core annotates `(UTC)`.
|
|
39
|
+
* - `userEmail`: trimmed `SEMA_USER_EMAIL`, only when non-empty. Never the empty string (that would read as a
|
|
40
|
+
* present-but-blank email to core). Absent env ⇒ omitted.
|
|
41
|
+
* Returns `{}` (no `clientContext` key) only when BOTH are absent, so a zone-less + email-less host keeps the
|
|
42
|
+
* request shape unchanged (core defaults to UTC annotation).
|
|
43
|
+
*/
|
|
44
|
+
export declare function clientContextField(timeZone: string | undefined, env?: EnvLike): {
|
|
45
|
+
clientContext?: ClientContext;
|
|
46
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/clientContextWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/clientContextWireCaps.ts — PURE projection: the user's LOCAL identity facts (time zone + optional
|
|
6
|
+
* email) → the `TaskRequest.clientContext` object the SERVICE relays to core `TaskSpec.clientContext`.
|
|
7
|
+
*
|
|
8
|
+
* THE GAP THIS CLOSES (design/112, core/search AI 2026-06-30 — core 1.181): the worker container runs UTC and
|
|
9
|
+
* knows neither the user's time zone nor their identity. Without this, core renders the `# Environment` block's
|
|
10
|
+
* "today" in CONTAINER time (UTC) — a TOB-fleet user in Asia/Tokyo sees the wrong date, and the model has no
|
|
11
|
+
* email to attribute work. core 1.181 added the seam `TaskSpec.clientContext?: { timeZone?, userEmail? }`; the
|
|
12
|
+
* CLIENT half is the shell stamping the user's real IANA zone (+ email when known) onto every request, so core
|
|
13
|
+
* localizes "today" to the user's zone and ANNOTATES it (e.g. `(Asia/Tokyo)`); absent ⇒ core annotates `(UTC)`.
|
|
14
|
+
*
|
|
15
|
+
* SECURITY/CONTRACT (core 1.186 hardening, blackboard 2026-06-30):
|
|
16
|
+
* - The `locale` field was a DEAD (zero-consumer) field in 1.181 and was DELETED in 1.186 — we NEVER set it
|
|
17
|
+
* (only `timeZone` + `userEmail` actually drive rendering). An invalid IANA zone is core's problem to
|
|
18
|
+
* validate (1.186: a malformed zone falls back to UTC + `(UTC)` rather than lying); the shell just forwards
|
|
19
|
+
* what `Intl` resolved (always a valid zone on a real host).
|
|
20
|
+
* - `userEmail` is benign identity metadata, NOT a secret/token (TaskRequest secret discipline still holds —
|
|
21
|
+
* no tokens cross here). Sourced from `SEMA_USER_EMAIL` only when the shell actually knows it; absent ⇒ omit.
|
|
22
|
+
*
|
|
23
|
+
* PURE — no module-scope IO of its OWN beyond the caller-supplied `timeZone` + `env`, so it bundles + unit-tests
|
|
24
|
+
* without the heavy settings/Intl graph (the caller passes the cached `getTimeZone()` from utils/intl). Mirrors
|
|
25
|
+
* the other *WireCaps projection modules (selfOrchestration/webSearch).
|
|
26
|
+
*/
|
|
27
|
+
import { hostEnv } from './hostEnv.js';
|
|
28
|
+
/** The env key the shell reads for the user's email (optional). SEMA_-namespaced. */
|
|
29
|
+
export const USER_EMAIL_ENV = 'SEMA_USER_EMAIL';
|
|
30
|
+
/**
|
|
31
|
+
* Build the `{ clientContext }` partial for `toTaskRequest` to spread.
|
|
32
|
+
* - `timeZone`: the caller's resolved IANA zone (utils/intl `getTimeZone()` — always a valid zone on a host).
|
|
33
|
+
* Omitted only if the host has no resolvable zone (degenerate stripped-ICU) so core annotates `(UTC)`.
|
|
34
|
+
* - `userEmail`: trimmed `SEMA_USER_EMAIL`, only when non-empty. Never the empty string (that would read as a
|
|
35
|
+
* present-but-blank email to core). Absent env ⇒ omitted.
|
|
36
|
+
* Returns `{}` (no `clientContext` key) only when BOTH are absent, so a zone-less + email-less host keeps the
|
|
37
|
+
* request shape unchanged (core defaults to UTC annotation).
|
|
38
|
+
*/
|
|
39
|
+
export function clientContextField(timeZone, env = hostEnv()) {
|
|
40
|
+
const rawEmail = env[USER_EMAIL_ENV];
|
|
41
|
+
const userEmail = typeof rawEmail === 'string' ? rawEmail.trim() : '';
|
|
42
|
+
const tz = typeof timeZone === 'string' ? timeZone.trim() : '';
|
|
43
|
+
const ctx = {
|
|
44
|
+
...(tz ? { timeZone: tz } : {}),
|
|
45
|
+
...(userEmail ? { userEmail } : {}),
|
|
46
|
+
};
|
|
47
|
+
return Object.keys(ctx).length > 0 ? { clientContext: ctx } : {};
|
|
48
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/cloudConfigWireCaps.ts 搬入(依赖 mcpWireCaps/skillsWireCaps,同批)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/cloudConfigWireCaps.ts — PURE projections for the CLOUD CONFIG WIRE (P2): the registry's
|
|
6
|
+
* GET /api/v1/effective body → the same wire shapes the LOCAL loaders produce, so flipping the
|
|
7
|
+
* config-source switch swaps the SOURCE without changing a single downstream contract:
|
|
8
|
+
*
|
|
9
|
+
* config.mcp.servers → `McpServerSpec[]` (== mcpWire loadMcpSpecs output → TaskRequest.mcpServers)
|
|
10
|
+
* config.skills.skills → `SkillSpec[]` (== skillsWire loadSkillSpecs output → TaskRequest.skills;
|
|
11
|
+
* the v1 wire is a MANIFEST — content replaced by contentHash —
|
|
12
|
+
* so the body is injected via `resolveBody(contentHash)`)
|
|
13
|
+
* config.models → config.d/models.json document (registry-core ModelsConfig IS the engine's
|
|
14
|
+
* FileConfigStore models.json shape — verbatim passthrough,
|
|
15
|
+
* written to a SHADOW config.d root, never the user's own)
|
|
16
|
+
* rosters → names only (v1 thinnest seam: display + reference; the /api/v1/effective wire
|
|
17
|
+
* STRIPS rosters — registry-core resolve-roster `shipped()` —
|
|
18
|
+
* so the IO half pulls the domain raw when it can)
|
|
19
|
+
*
|
|
20
|
+
* SECRET BOUNDARY: the registry NEVER ships secret values — mcp transports carry `envRefs`/`headerRefs`
|
|
21
|
+
* (target key → an ENV NAME) and models carry `apiKeyEnv`. The projection resolves env-NAME refs against the
|
|
22
|
+
* SHELL's env (the engine child inherits it), and reports the names that resolve to nothing so callers can warn.
|
|
23
|
+
*
|
|
24
|
+
* Split from cloudConfigWire.ts (the IO half: token/fetch/cache/shadow-write) so this file is esbuild-bundleable
|
|
25
|
+
* alone for the assertion suite — it imports only erased types + the sibling pure caps modules.
|
|
26
|
+
*/
|
|
27
|
+
import type { SkillSpec } from '@sema-agent/sdk';
|
|
28
|
+
import type { McpServerSpec } from './mcpWireCaps.js';
|
|
29
|
+
import type { EnvLike } from './hostEnv.js';
|
|
30
|
+
/** The SDK wire shape TaskRequest.mcpServers carries (the SDK barrel doesn't re-export McpServerSpec by
|
|
31
|
+
* name — derive it from TaskRequest so this stays the EXACT wire type, never a drifting copy). */
|
|
32
|
+
export type { McpServerSpec };
|
|
33
|
+
type Rec = Record<string, unknown>;
|
|
34
|
+
/** Env lookup shape (process.env or a test fixture). */
|
|
35
|
+
export type { EnvLike };
|
|
36
|
+
export interface CloudMcpProjection {
|
|
37
|
+
specs: McpServerSpec[];
|
|
38
|
+
/** env NAMES referenced by envRefs/headerRefs that resolve to nothing in `env` (the ref is DROPPED —
|
|
39
|
+
* fail-closed on the value, loud in the caller's warning line). */
|
|
40
|
+
missingEnv: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project the registry's mcp domain (`config.mcp.servers`, registry-core McpServerSpec — the CENTER shape
|
|
44
|
+
* with envRefs/headerRefs) → the SDK/core wire `McpServerSpec[]` (real env/header VALUES, resolved from the
|
|
45
|
+
* shell env). Disabled servers are skipped; unknown transport kinds are skipped (never mis-mapped); the
|
|
46
|
+
* result is capped at MCP_CAPS.items exactly like the local `.mcp.json` projection.
|
|
47
|
+
*/
|
|
48
|
+
export declare function cloudMcpToSpecs(servers: unknown, env: EnvLike): CloudMcpProjection;
|
|
49
|
+
/**
|
|
50
|
+
* Project the /api/v1/effective skills MANIFEST (content replaced by `contentHash` — registry-core
|
|
51
|
+
* toSkillsManifest) → the capped wire `SkillSpec[]`. `resolveBody(contentHash)` supplies each body (the IO
|
|
52
|
+
* half caches bodies by hash; null ⇒ the skill is SKIPPED — a nameless/bodyless SkillSpec is a 400). Same
|
|
53
|
+
* caps + dedupe as the local projection (skillsWireCaps), so the wire is well-formed by construction.
|
|
54
|
+
*/
|
|
55
|
+
export declare function cloudSkillsToSpecs(manifestSkills: unknown, resolveBody: (contentHash: string) => string | null): SkillSpec[];
|
|
56
|
+
/**
|
|
57
|
+
* The registry's models domain (registry-core ModelsConfig) IS the engine's `config.d/models.json` contract
|
|
58
|
+
* (the same zod schema parses both — engine CONFIG_PROVIDER=local FileConfigStore lane), so the projection is
|
|
59
|
+
* a VALIDATION-LITE PASSTHROUGH: keep exactly {models, roles, atModelAllowlist}, drop everything else, and
|
|
60
|
+
* return null when there is no well-formed models array (caller then writes an empty catalog — engine
|
|
61
|
+
* semantics: no enabled models ⇒ env-catalog fallback).
|
|
62
|
+
*/
|
|
63
|
+
export declare function cloudModelsToDoc(models: unknown): Rec | null;
|
|
64
|
+
/** ENABLED model names in a models doc (banner/status display + "did the cloud catalog apply" checks). */
|
|
65
|
+
export declare function cloudModelNames(doc: Rec | null): string[];
|
|
66
|
+
/** `apiKeyEnv` NAMES referenced by enabled models that resolve to nothing in `env` — the engine child would
|
|
67
|
+
* route those models with no key (a mid-session 401 the user can't diagnose); callers warn up front. */
|
|
68
|
+
export declare function cloudModelMissingKeyEnv(doc: Rec | null, env: EnvLike): string[];
|
|
69
|
+
export interface CloudSessionMirrorProjection {
|
|
70
|
+
engineUrl: string;
|
|
71
|
+
required: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface CloudExecutionProjection {
|
|
74
|
+
required: boolean;
|
|
75
|
+
allowedLanes: string[];
|
|
76
|
+
/** registry-core 0.10.1 `execution.sessionMirror`(附B 单C):org 默认会话镜像目标两层配置。
|
|
77
|
+
* null = 未下发/形状不对(INERT — 缺省零行为差;misshapen 绝不猜出一个镜像目标)。 */
|
|
78
|
+
sessionMirror: CloudSessionMirrorProjection | null;
|
|
79
|
+
}
|
|
80
|
+
/** Project the registry's execution domain (registry-core 0.8 ExecutionPolicy — `{required, allowedLanes}`,
|
|
81
|
+
* allowedLanes = OPEN string vocab, [580]④; 0.10.1 adds `sessionMirror{engineUrl, required}` — additive).
|
|
82
|
+
* Defensive like every other projection: a missing/misshapen domain (an 0.7 registry) yields the schema
|
|
83
|
+
* defaults `{required:false, allowedLanes:["host"], sessionMirror:null}` — never a throw, never an
|
|
84
|
+
* accidental lock-ON, and never a guessed mirror target (engineUrl must be a non-empty string or the
|
|
85
|
+
* whole sessionMirror is dropped: a mirror carries session content + a Bearer, we don't improvise one). */
|
|
86
|
+
export declare function cloudExecutionToPolicy(execution: unknown): CloudExecutionProjection;
|
|
87
|
+
/** Roster names from a rosters domain value ({rosters:[{name,…}]}). The /api/v1/effective wire strips
|
|
88
|
+
* rosters (control-plane only), so the IO half feeds this from the raw scoped domain GET when readable. */
|
|
89
|
+
export declare function cloudRosterNames(rostersValue: unknown): string[];
|
|
90
|
+
export interface CloudProjection {
|
|
91
|
+
mcp: McpServerSpec[];
|
|
92
|
+
skills: SkillSpec[];
|
|
93
|
+
/** The shadow config.d/models.json document (null = registry shipped no well-formed models domain). */
|
|
94
|
+
modelsDoc: Rec | null;
|
|
95
|
+
modelNames: string[];
|
|
96
|
+
rosterNames: string[];
|
|
97
|
+
/** env NAMES referenced by the cloud config (mcp refs + model apiKeyEnv) missing from the shell env. */
|
|
98
|
+
missingEnv: string[];
|
|
99
|
+
scope: string;
|
|
100
|
+
/** Skills present in the manifest whose body could not be resolved (skipped on the wire). */
|
|
101
|
+
skillsMissingBody: string[];
|
|
102
|
+
/** S3 execution-lane policy ([580]① / registry-core 0.8). Consumed by the sandbox policy-lock seam;
|
|
103
|
+
* ⚠️ client-enforced ADVISORY (P5=A) — the engine gate is service [580]. */
|
|
104
|
+
execution: CloudExecutionProjection;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Project one /api/v1/effective BODY (fresh or cached — the fallback path MUST produce the identical shape)
|
|
108
|
+
* into everything the shell consumes. Pure: bodies + env are injected.
|
|
109
|
+
*/
|
|
110
|
+
export declare function projectEffectiveBody(body: unknown, skillBodies: Record<string, string>, env: EnvLike, rostersValue?: unknown): CloudProjection;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { MCP_CAPS } from './mcpWireCaps.js';
|
|
2
|
+
import { SKILL_CAPS, toCappedSpec } from './skillsWireCaps.js';
|
|
3
|
+
const asRec = (v) => (typeof v === 'object' && v !== null && !Array.isArray(v) ? v : undefined);
|
|
4
|
+
const asArr = (v) => (Array.isArray(v) ? v : []);
|
|
5
|
+
const asStr = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
6
|
+
/**
|
|
7
|
+
* Project the registry's mcp domain (`config.mcp.servers`, registry-core McpServerSpec — the CENTER shape
|
|
8
|
+
* with envRefs/headerRefs) → the SDK/core wire `McpServerSpec[]` (real env/header VALUES, resolved from the
|
|
9
|
+
* shell env). Disabled servers are skipped; unknown transport kinds are skipped (never mis-mapped); the
|
|
10
|
+
* result is capped at MCP_CAPS.items exactly like the local `.mcp.json` projection.
|
|
11
|
+
*/
|
|
12
|
+
export function cloudMcpToSpecs(servers, env) {
|
|
13
|
+
const specs = [];
|
|
14
|
+
const missingEnv = [];
|
|
15
|
+
for (const raw of asArr(servers)) {
|
|
16
|
+
if (specs.length >= MCP_CAPS.items)
|
|
17
|
+
break;
|
|
18
|
+
const s = asRec(raw);
|
|
19
|
+
if (!s || s.enabled === false)
|
|
20
|
+
continue;
|
|
21
|
+
const name = asStr(s.name);
|
|
22
|
+
const t = asRec(s.transport);
|
|
23
|
+
if (!name || !t)
|
|
24
|
+
continue;
|
|
25
|
+
let transport = null;
|
|
26
|
+
if (t.kind === 'stdio') {
|
|
27
|
+
const command = asStr(t.command);
|
|
28
|
+
if (!command)
|
|
29
|
+
continue;
|
|
30
|
+
// envRefs: target ENV key → ENV NAME holding the value (center never stores the secret). Resolve
|
|
31
|
+
// against the shell env — the engine child inherits it, so stdio spawn sees the same values.
|
|
32
|
+
const envOut = {};
|
|
33
|
+
for (const [target, ref] of Object.entries(asRec(t.envRefs) ?? {})) {
|
|
34
|
+
const refName = asStr(ref);
|
|
35
|
+
if (!refName)
|
|
36
|
+
continue;
|
|
37
|
+
const v = env[refName];
|
|
38
|
+
if (v === undefined || v === '')
|
|
39
|
+
missingEnv.push(refName);
|
|
40
|
+
else
|
|
41
|
+
envOut[target] = v;
|
|
42
|
+
}
|
|
43
|
+
const args = asArr(t.args).filter((a) => typeof a === 'string');
|
|
44
|
+
transport = {
|
|
45
|
+
kind: 'stdio',
|
|
46
|
+
command,
|
|
47
|
+
...(args.length > 0 ? { args } : {}),
|
|
48
|
+
...(Object.keys(envOut).length > 0 ? { env: envOut } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else if (t.kind === 'http') {
|
|
52
|
+
const url = asStr(t.url);
|
|
53
|
+
if (!url)
|
|
54
|
+
continue;
|
|
55
|
+
const headers = {};
|
|
56
|
+
for (const [headerName, ref] of Object.entries(asRec(t.headerRefs) ?? {})) {
|
|
57
|
+
const refName = asStr(ref);
|
|
58
|
+
if (!refName)
|
|
59
|
+
continue;
|
|
60
|
+
const v = env[refName];
|
|
61
|
+
if (v === undefined || v === '')
|
|
62
|
+
missingEnv.push(refName);
|
|
63
|
+
else
|
|
64
|
+
headers[headerName] = v;
|
|
65
|
+
}
|
|
66
|
+
const principalHeader = asStr(t.principalHeader);
|
|
67
|
+
transport = {
|
|
68
|
+
kind: 'http',
|
|
69
|
+
url,
|
|
70
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
71
|
+
...(principalHeader ? { principalHeader } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
continue; // unknown transport kind — skip, never mis-map (same posture as the local sse/ws skip)
|
|
76
|
+
}
|
|
77
|
+
const allowTools = asArr(s.allowTools).filter((a) => typeof a === 'string');
|
|
78
|
+
specs.push({
|
|
79
|
+
name,
|
|
80
|
+
transport,
|
|
81
|
+
...(allowTools.length > 0 ? { allowTools } : {}),
|
|
82
|
+
...(s.elicitation === true ? { elicitation: true } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { specs, missingEnv: [...new Set(missingEnv)] };
|
|
86
|
+
}
|
|
87
|
+
// ── skills: config.skills.skills (MANIFEST) + bodies → SkillSpec[] ───────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Project the /api/v1/effective skills MANIFEST (content replaced by `contentHash` — registry-core
|
|
90
|
+
* toSkillsManifest) → the capped wire `SkillSpec[]`. `resolveBody(contentHash)` supplies each body (the IO
|
|
91
|
+
* half caches bodies by hash; null ⇒ the skill is SKIPPED — a nameless/bodyless SkillSpec is a 400). Same
|
|
92
|
+
* caps + dedupe as the local projection (skillsWireCaps), so the wire is well-formed by construction.
|
|
93
|
+
*/
|
|
94
|
+
export function cloudSkillsToSpecs(manifestSkills, resolveBody) {
|
|
95
|
+
const specs = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const raw of asArr(manifestSkills)) {
|
|
98
|
+
if (specs.length >= SKILL_CAPS.items)
|
|
99
|
+
break;
|
|
100
|
+
const s = asRec(raw);
|
|
101
|
+
if (!s || s.enabled === false)
|
|
102
|
+
continue;
|
|
103
|
+
const name = asStr(s.name);
|
|
104
|
+
const hash = asStr(s.contentHash);
|
|
105
|
+
if (!name || !hash)
|
|
106
|
+
continue;
|
|
107
|
+
const body = resolveBody(hash);
|
|
108
|
+
if (!body)
|
|
109
|
+
continue;
|
|
110
|
+
const spec = toCappedSpec(name, asStr(s.description) ?? '', '', body);
|
|
111
|
+
if (!spec || seen.has(spec.name))
|
|
112
|
+
continue;
|
|
113
|
+
seen.add(spec.name);
|
|
114
|
+
specs.push(spec);
|
|
115
|
+
}
|
|
116
|
+
return specs;
|
|
117
|
+
}
|
|
118
|
+
// ── models: config.models → shadow config.d/models.json document ─────────────────────────────────────────
|
|
119
|
+
/**
|
|
120
|
+
* The registry's models domain (registry-core ModelsConfig) IS the engine's `config.d/models.json` contract
|
|
121
|
+
* (the same zod schema parses both — engine CONFIG_PROVIDER=local FileConfigStore lane), so the projection is
|
|
122
|
+
* a VALIDATION-LITE PASSTHROUGH: keep exactly {models, roles, atModelAllowlist}, drop everything else, and
|
|
123
|
+
* return null when there is no well-formed models array (caller then writes an empty catalog — engine
|
|
124
|
+
* semantics: no enabled models ⇒ env-catalog fallback).
|
|
125
|
+
*/
|
|
126
|
+
export function cloudModelsToDoc(models) {
|
|
127
|
+
const m = asRec(models);
|
|
128
|
+
if (!m || !Array.isArray(m.models))
|
|
129
|
+
return null;
|
|
130
|
+
const entries = m.models.filter((e) => {
|
|
131
|
+
const r = asRec(e);
|
|
132
|
+
return !!r && !!asStr(r.name) && !!asStr(r.id);
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
models: entries,
|
|
136
|
+
...(asRec(m.roles) ? { roles: m.roles } : {}),
|
|
137
|
+
...(Array.isArray(m.atModelAllowlist) ? { atModelAllowlist: m.atModelAllowlist } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** ENABLED model names in a models doc (banner/status display + "did the cloud catalog apply" checks). */
|
|
141
|
+
export function cloudModelNames(doc) {
|
|
142
|
+
if (!doc)
|
|
143
|
+
return [];
|
|
144
|
+
return asArr(doc.models)
|
|
145
|
+
.map((e) => {
|
|
146
|
+
const r = asRec(e);
|
|
147
|
+
return r && r.enabled !== false ? asStr(r.name) : undefined;
|
|
148
|
+
})
|
|
149
|
+
.filter((n) => !!n);
|
|
150
|
+
}
|
|
151
|
+
/** `apiKeyEnv` NAMES referenced by enabled models that resolve to nothing in `env` — the engine child would
|
|
152
|
+
* route those models with no key (a mid-session 401 the user can't diagnose); callers warn up front. */
|
|
153
|
+
export function cloudModelMissingKeyEnv(doc, env) {
|
|
154
|
+
if (!doc)
|
|
155
|
+
return [];
|
|
156
|
+
const out = new Set();
|
|
157
|
+
for (const e of asArr(doc.models)) {
|
|
158
|
+
const r = asRec(e);
|
|
159
|
+
if (!r || r.enabled === false)
|
|
160
|
+
continue;
|
|
161
|
+
const ref = asStr(r.apiKeyEnv);
|
|
162
|
+
if (ref && (env[ref] === undefined || env[ref] === ''))
|
|
163
|
+
out.add(ref);
|
|
164
|
+
}
|
|
165
|
+
return [...out];
|
|
166
|
+
}
|
|
167
|
+
/** Project the registry's execution domain (registry-core 0.8 ExecutionPolicy — `{required, allowedLanes}`,
|
|
168
|
+
* allowedLanes = OPEN string vocab, [580]④; 0.10.1 adds `sessionMirror{engineUrl, required}` — additive).
|
|
169
|
+
* Defensive like every other projection: a missing/misshapen domain (an 0.7 registry) yields the schema
|
|
170
|
+
* defaults `{required:false, allowedLanes:["host"], sessionMirror:null}` — never a throw, never an
|
|
171
|
+
* accidental lock-ON, and never a guessed mirror target (engineUrl must be a non-empty string or the
|
|
172
|
+
* whole sessionMirror is dropped: a mirror carries session content + a Bearer, we don't improvise one). */
|
|
173
|
+
export function cloudExecutionToPolicy(execution) {
|
|
174
|
+
const e = asRec(execution);
|
|
175
|
+
const lanes = asArr(e?.allowedLanes).filter((l) => typeof l === 'string' && l.length > 0);
|
|
176
|
+
const m = asRec(e?.sessionMirror);
|
|
177
|
+
const mirrorUrl = asStr(m?.engineUrl);
|
|
178
|
+
return {
|
|
179
|
+
required: e?.required === true,
|
|
180
|
+
allowedLanes: lanes.length > 0 ? lanes : ['host'],
|
|
181
|
+
// trailing-slash normalization matches engineTarget.normalizeEngineUrl (origin/no-op comparisons downstream)
|
|
182
|
+
sessionMirror: mirrorUrl ? { engineUrl: mirrorUrl.replace(/\/+$/, ''), required: m?.required === true } : null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// ── rosters: v1 thinnest seam (names for display / model-order reference) ────────────────────────────────
|
|
186
|
+
/** Roster names from a rosters domain value ({rosters:[{name,…}]}). The /api/v1/effective wire strips
|
|
187
|
+
* rosters (control-plane only), so the IO half feeds this from the raw scoped domain GET when readable. */
|
|
188
|
+
export function cloudRosterNames(rostersValue) {
|
|
189
|
+
const r = asRec(rostersValue);
|
|
190
|
+
return asArr(r?.rosters)
|
|
191
|
+
.map((e) => asStr(asRec(e)?.name))
|
|
192
|
+
.filter((n) => !!n);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Project one /api/v1/effective BODY (fresh or cached — the fallback path MUST produce the identical shape)
|
|
196
|
+
* into everything the shell consumes. Pure: bodies + env are injected.
|
|
197
|
+
*/
|
|
198
|
+
export function projectEffectiveBody(body, skillBodies, env, rostersValue) {
|
|
199
|
+
const b = asRec(body) ?? {};
|
|
200
|
+
const config = asRec(b.config) ?? {};
|
|
201
|
+
const mcpDomain = asRec(config.mcp);
|
|
202
|
+
const skillsDomain = asRec(config.skills);
|
|
203
|
+
const { specs: mcp, missingEnv: mcpMissing } = cloudMcpToSpecs(mcpDomain?.servers, env);
|
|
204
|
+
const manifest = asArr(skillsDomain?.skills);
|
|
205
|
+
const skills = cloudSkillsToSpecs(manifest, (hash) => skillBodies[hash] ?? null);
|
|
206
|
+
const skillsMissingBody = manifest
|
|
207
|
+
.map((raw) => {
|
|
208
|
+
const s = asRec(raw);
|
|
209
|
+
if (!s || s.enabled === false)
|
|
210
|
+
return undefined;
|
|
211
|
+
const name = asStr(s.name);
|
|
212
|
+
const hash = asStr(s.contentHash);
|
|
213
|
+
return name && hash && skillBodies[hash] === undefined ? name : undefined;
|
|
214
|
+
})
|
|
215
|
+
.filter((n) => !!n);
|
|
216
|
+
const modelsDoc = cloudModelsToDoc(config.models);
|
|
217
|
+
return {
|
|
218
|
+
mcp,
|
|
219
|
+
skills,
|
|
220
|
+
modelsDoc,
|
|
221
|
+
modelNames: cloudModelNames(modelsDoc),
|
|
222
|
+
rosterNames: cloudRosterNames(rostersValue),
|
|
223
|
+
missingEnv: [...new Set([...mcpMissing, ...cloudModelMissingKeyEnv(modelsDoc, env)])],
|
|
224
|
+
scope: asStr(b.scope) ?? 'global',
|
|
225
|
+
skillsMissingBody,
|
|
226
|
+
execution: cloudExecutionToPolicy(config.execution),
|
|
227
|
+
};
|
|
228
|
+
}
|