@lqc123qwe/car-runtime 1.8.0 → 1.10.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 +29 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +44 -0
- package/dist/authz/authz.js +65 -0
- package/dist/car-runtime-1.10.0.tgz +0 -0
- package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +79 -0
- package/dist/loop/stop.js +151 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +40 -0
- package/dist/runtime-core/chatStep.js +104 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +553 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +84 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +21 -0
- package/dist/seam.js +20 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/mcp/gateway.ts +10 -2
- package/src/mcp/httpClient.ts +23 -5
- package/src/runtime-core/trace.ts +5 -0
- package/src/seam.ts +21 -0
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F13 · Goal 层(五层停止第五层):phase 持久化 + activation 进程内
|
|
3
|
+
*
|
|
4
|
+
* 口径(dsh goal/goal/src/types.ts:45,75,87-89 源码级已核实 + M2 系统设计增补 T-1):
|
|
5
|
+
* - GoalPhase = active | paused | blocked | complete —— 持久化:经会话日志 goalUpdate 事件
|
|
6
|
+
* - GoalActivation = armed | disarmed —— 进程内、never persisted:
|
|
7
|
+
* 新实例默认 disarmed = 「session-start 重置 disarmed」零分支天然满足(AC-4.5)
|
|
8
|
+
* - update_goal(complete) 不立刻掐断 turn:先写持久状态,turn 由既有收口逻辑完成(D5 §9)
|
|
9
|
+
* - blocked 产出点:授权门 blockOnDeny / 策略门(stop.ts 集成)
|
|
10
|
+
*/
|
|
11
|
+
import type { SessionLog, SessionEvent } from '../session/log.ts';
|
|
12
|
+
export type GoalPhase = 'active' | 'paused' | 'blocked' | 'complete';
|
|
13
|
+
export type GoalActivation = 'armed' | 'disarmed';
|
|
14
|
+
export declare class GoalDriver {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(log: SessionLog);
|
|
17
|
+
get phase(): GoalPhase;
|
|
18
|
+
get activation(): GoalActivation;
|
|
19
|
+
/** 持久状态变更:写 goalUpdate 事件(重启后由恢复层读取重放) */
|
|
20
|
+
setPhase(phase: GoalPhase, reason: string): void;
|
|
21
|
+
/** 进程内激活(不持久化——重启后默认 disarmed) */
|
|
22
|
+
arm(): void;
|
|
23
|
+
disarm(reason: string): void;
|
|
24
|
+
/** 超限/撞限/错误自动 disarm(D5 §9:Round 超限、max-tokens、Agent error 都会 disarm) */
|
|
25
|
+
autoDisarmOnTurnEnd(reason: string): void;
|
|
26
|
+
/** 恢复层:从已落盘日志重放 goalUpdate 重建 phase(activation 不恢复——保持 disarmed) */
|
|
27
|
+
static replay(events: readonly SessionEvent[]): {
|
|
28
|
+
phase: GoalPhase;
|
|
29
|
+
activation: GoalActivation;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class GoalDriver {
|
|
2
|
+
#log;
|
|
3
|
+
#phase = 'active';
|
|
4
|
+
#activation = 'disarmed'; // 重启/新实例默认 disarmed(不会把历史任务叫醒)
|
|
5
|
+
#disarmReasons = [];
|
|
6
|
+
constructor(log) { this.#log = log; }
|
|
7
|
+
get phase() { return this.#phase; }
|
|
8
|
+
get activation() { return this.#activation; }
|
|
9
|
+
/** 持久状态变更:写 goalUpdate 事件(重启后由恢复层读取重放) */
|
|
10
|
+
setPhase(phase, reason) {
|
|
11
|
+
this.#phase = phase;
|
|
12
|
+
this.#log.append('runtime', 'goalUpdate', 'GOAL', { phase, reason });
|
|
13
|
+
}
|
|
14
|
+
/** 进程内激活(不持久化——重启后默认 disarmed) */
|
|
15
|
+
arm() { this.#activation = 'armed'; }
|
|
16
|
+
disarm(reason) {
|
|
17
|
+
this.#activation = 'disarmed';
|
|
18
|
+
this.#disarmReasons.push(reason);
|
|
19
|
+
}
|
|
20
|
+
/** 超限/撞限/错误自动 disarm(D5 §9:Round 超限、max-tokens、Agent error 都会 disarm) */
|
|
21
|
+
autoDisarmOnTurnEnd(reason) {
|
|
22
|
+
if (reason !== 'completed')
|
|
23
|
+
this.disarm(`turn ended with ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
/** 恢复层:从已落盘日志重放 goalUpdate 重建 phase(activation 不恢复——保持 disarmed) */
|
|
26
|
+
static replay(events) {
|
|
27
|
+
let phase = 'active';
|
|
28
|
+
for (const e of events) {
|
|
29
|
+
if (e.kind === 'goalUpdate' && e.payload?.phase)
|
|
30
|
+
phase = e.payload.phase;
|
|
31
|
+
}
|
|
32
|
+
return { phase, activation: 'disarmed' }; // activation 永不持久化
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F13 · 恢复层:interrupted 补记(进程消失后恢复层给未闭合 turn 补状态)
|
|
3
|
+
*
|
|
4
|
+
* 口径(D4 §18 / D5 §19):
|
|
5
|
+
* - interrupted = 进程消失后恢复层补记,与 aborted(运行期取消可清理)不可混用
|
|
6
|
+
* - 未配对 toolCall 补合成错误 toolResult(日志无缺口红线)
|
|
7
|
+
* - 副作用审计:补记事件带 recovered=true 标记(副作用是否落地由恢复层显式声明为未知)
|
|
8
|
+
*/
|
|
9
|
+
import type { SessionLog } from '../session/log.ts';
|
|
10
|
+
export interface RecoveryReport {
|
|
11
|
+
/** 补记 interrupted 的 turn 数 */
|
|
12
|
+
turnsClosed: number;
|
|
13
|
+
/** 补记合成 toolResult 的未配对调用数 */
|
|
14
|
+
pairedCalls: number;
|
|
15
|
+
}
|
|
16
|
+
/** 扫描日志中未闭合的 turn(有 turnStart 语义的用户事件但没有 turnEnd)与未配对 toolCall */
|
|
17
|
+
export declare function recoverInterrupted(log: SessionLog): RecoveryReport;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** 扫描日志中未闭合的 turn(有 turnStart 语义的用户事件但没有 turnEnd)与未配对 toolCall */
|
|
2
|
+
export function recoverInterrupted(log) {
|
|
3
|
+
const report = { turnsClosed: 0, pairedCalls: 0 };
|
|
4
|
+
const callIds = new Set();
|
|
5
|
+
const resultIds = new Set();
|
|
6
|
+
const openTurns = new Set();
|
|
7
|
+
const closedTurns = new Set();
|
|
8
|
+
for (const e of log.events) {
|
|
9
|
+
if (e.kind === 'toolCall')
|
|
10
|
+
callIds.add(e.payload.id);
|
|
11
|
+
if (e.kind === 'toolResult')
|
|
12
|
+
resultIds.add(e.payload.id);
|
|
13
|
+
if (e.kind === 'user')
|
|
14
|
+
openTurns.add(e.turnId);
|
|
15
|
+
if (e.kind === 'turnEnd') {
|
|
16
|
+
closedTurns.add(e.turnId);
|
|
17
|
+
openTurns.delete(e.turnId);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// 未配对 toolCall → 合成错误 toolResult(副作用状态未知:recovered=true 显式声明)
|
|
21
|
+
for (const e of log.events) {
|
|
22
|
+
if (e.kind !== 'toolCall')
|
|
23
|
+
continue;
|
|
24
|
+
const id = e.payload.id;
|
|
25
|
+
if (resultIds.has(id))
|
|
26
|
+
continue;
|
|
27
|
+
if (closedTurns.has(e.turnId))
|
|
28
|
+
continue; // 已闭合 turn 内成对由原逻辑保证
|
|
29
|
+
log.append('runtime', 'toolResult', e.turnId, { id, error: 'recovered: process interrupted before dispatch result', recovered: true });
|
|
30
|
+
resultIds.add(id);
|
|
31
|
+
report.pairedCalls++;
|
|
32
|
+
}
|
|
33
|
+
// 未闭合 turn → 补记 turnEnd=interrupted(不可与 aborted 混用)
|
|
34
|
+
for (const turnId of openTurns) {
|
|
35
|
+
log.append('runtime', 'turnEnd', turnId, null, { reason: 'interrupted', recovered: true });
|
|
36
|
+
report.turnsClosed++;
|
|
37
|
+
}
|
|
38
|
+
return report;
|
|
39
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F5 · 最小 turn 级停止语义(ADR-001 实装)
|
|
3
|
+
*
|
|
4
|
+
* 口径(《系统设计》F5 / D4-D5 机制对照 / POC-3 I4):
|
|
5
|
+
* - step 三结果:completed / max-tokens / null(null = 工具结果交回模型,非异常)
|
|
6
|
+
* - TurnEndReason 六值:completed / max-tokens / aborted / error / blocked / interrupted
|
|
7
|
+
* (aborted=运行期取消可清理;interrupted=进程消失后恢复层补记——不可混用,S1/S2 仅实装前者)
|
|
8
|
+
* - ADR-001:max-tokens 解析前收口(截断响应不解析工具调用,禁补全半截 JSON);
|
|
9
|
+
* 只读白名单可选补错重试(preset.maxTokens.retryReadOnly);endReasonTrail 保守判定:
|
|
10
|
+
* turn 内任一 step 撞限 → turn/end 保留 max-tokens,不被后续成功 step 冲销
|
|
11
|
+
* - 工具收口:concludesTurn=OR(任一 true 即收口);terminate=AND(整批 finalized 全 true 才中断)
|
|
12
|
+
* - 取消:未派发调用补记成对事件(toolCall + 合成错误 toolResult),日志无缺口
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionLog } from '../session/log.ts';
|
|
15
|
+
export type StepOutcome = 'completed' | 'max-tokens' | 'null';
|
|
16
|
+
export type TurnEndReason = 'completed' | 'max-tokens' | 'aborted' | 'error' | 'blocked' | 'interrupted';
|
|
17
|
+
export type SideEffect = 'readonly' | 'write';
|
|
18
|
+
export interface ToolCall {
|
|
19
|
+
id: string;
|
|
20
|
+
tool: string;
|
|
21
|
+
args: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface ToolDef {
|
|
24
|
+
declaredSideEffect: SideEffect;
|
|
25
|
+
concludesTurn?: boolean;
|
|
26
|
+
terminate?: boolean;
|
|
27
|
+
run(args: Record<string, unknown>): Promise<unknown>;
|
|
28
|
+
description?: string;
|
|
29
|
+
parameters?: unknown;
|
|
30
|
+
}
|
|
31
|
+
/** 模型单步响应(provider 形态;stopReason 为不可变透传——ADR-001) */
|
|
32
|
+
export interface ModelStep {
|
|
33
|
+
text?: string;
|
|
34
|
+
stopReason: 'stop' | 'length' | 'toolUse' | 'error';
|
|
35
|
+
/** 仅 stopReason=toolUse 时有意义;length 时 provider 可能给半截信息(CAR 不解析其 args) */
|
|
36
|
+
toolCalls?: ToolCall[];
|
|
37
|
+
/** length 时 provider 给出的「截断工具调用」:只有名字,args 不可信 */
|
|
38
|
+
truncatedTools?: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
tool: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export interface TurnPreset {
|
|
44
|
+
/** ADR-001:只读白名单补错重试开关(默认关) */
|
|
45
|
+
maxTokensRetryReadOnly?: boolean;
|
|
46
|
+
/** 权限模式(F4 集成点):confirm 下写操作需审批回调放行 */
|
|
47
|
+
mode?: 'readonly' | 'confirm' | 'full';
|
|
48
|
+
/** 确认模式审批回调:返回 true 放行 */
|
|
49
|
+
authorize?: (call: ToolCall) => Promise<boolean>;
|
|
50
|
+
maxSteps?: number;
|
|
51
|
+
/** F13:OR/AND 收口可配置(默认 any/all = M1 行为等价) */
|
|
52
|
+
aggregate?: {
|
|
53
|
+
concludesTurn?: 'any' | 'all';
|
|
54
|
+
terminate?: 'any' | 'all';
|
|
55
|
+
};
|
|
56
|
+
/** F13:授权拒绝即收口 blocked(默认 false = 模型继续调整) */
|
|
57
|
+
blockOnDeny?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface TurnResult {
|
|
60
|
+
reason: TurnEndReason;
|
|
61
|
+
steps: number;
|
|
62
|
+
endReasonTrail: Array<{
|
|
63
|
+
seq: number;
|
|
64
|
+
reason: TurnEndReason;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
export declare class TurnAborted extends Error {
|
|
68
|
+
constructor();
|
|
69
|
+
}
|
|
70
|
+
export declare function runTurn(opts: {
|
|
71
|
+
log: SessionLog;
|
|
72
|
+
turnId: string;
|
|
73
|
+
model: () => Promise<ModelStep>;
|
|
74
|
+
tools: Map<string, ToolDef>;
|
|
75
|
+
preset?: TurnPreset;
|
|
76
|
+
signal?: {
|
|
77
|
+
aborted: boolean;
|
|
78
|
+
};
|
|
79
|
+
}): Promise<TurnResult>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { CarM8Error } from '../runtime-core/errors.js';
|
|
2
|
+
export class TurnAborted extends Error {
|
|
3
|
+
constructor() { super('CAR-ABORTED: turn cancelled by user'); }
|
|
4
|
+
}
|
|
5
|
+
export async function runTurn(opts) {
|
|
6
|
+
const { log, turnId, model, tools, preset = {}, signal } = opts;
|
|
7
|
+
const maxSteps = preset.maxSteps ?? 32;
|
|
8
|
+
const trail = [];
|
|
9
|
+
let sawMaxTokens = false;
|
|
10
|
+
let steps = 0;
|
|
11
|
+
const executeBatch = async (calls) => {
|
|
12
|
+
let concludesTurn = false;
|
|
13
|
+
let concludesVotes = 0;
|
|
14
|
+
let terminateVotes = 0;
|
|
15
|
+
let finalized = 0;
|
|
16
|
+
let denies = 0;
|
|
17
|
+
for (const call of calls) {
|
|
18
|
+
if (signal?.aborted) {
|
|
19
|
+
// 取消:未派发调用补记成对事件(日志无缺口)
|
|
20
|
+
log.append('runtime', 'toolCall', turnId, call);
|
|
21
|
+
log.append('runtime', 'toolResult', turnId, { id: call.id, error: 'aborted-before-dispatch' });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const def = tools.get(call.tool);
|
|
25
|
+
log.append('model', 'toolCall', turnId, call);
|
|
26
|
+
if (!def) {
|
|
27
|
+
log.append('plugin', 'toolResult', turnId, { id: call.id, error: `unknown tool "${call.tool}"` });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// 权限门(F4 集成点):write 类在 readonly/confirm 模式需放行
|
|
31
|
+
if (def.declaredSideEffect === 'write' && preset.mode !== 'full') {
|
|
32
|
+
const granted = preset.mode === 'confirm' ? await (preset.authorize?.(call) ?? Promise.resolve(false)) : false;
|
|
33
|
+
log.append('runtime', 'toolResult', turnId, { id: call.id, error: 'authorization-denied', granted, mode: preset.mode });
|
|
34
|
+
if (!granted) {
|
|
35
|
+
denies++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 工具错误 = 成对 toolResult 交回模型(US-5/D5 语义:工具错误是结果而非 turn 中断——
|
|
40
|
+
// 与取消补记成对事件同纪律;turn 级 error 仅保留给模型请求/协议层失败)
|
|
41
|
+
let result;
|
|
42
|
+
let toolError;
|
|
43
|
+
try {
|
|
44
|
+
result = await def.run(call.args);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
result = undefined;
|
|
48
|
+
toolError = String(e);
|
|
49
|
+
}
|
|
50
|
+
log.append('plugin', 'toolResult', turnId, { id: call.id, result, ...(toolError ? { error: toolError } : {}) });
|
|
51
|
+
finalized++;
|
|
52
|
+
if (def.concludesTurn) {
|
|
53
|
+
concludesTurn = true;
|
|
54
|
+
concludesVotes++;
|
|
55
|
+
}
|
|
56
|
+
if (def.terminate)
|
|
57
|
+
terminateVotes++;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
concludesTurn, concludesAll: finalized > 0 && concludesVotes === finalized,
|
|
61
|
+
terminateVotes, terminateAll: finalized > 0 && terminateVotes === finalized,
|
|
62
|
+
finalized, denies,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
while (steps < maxSteps) {
|
|
67
|
+
if (signal?.aborted) {
|
|
68
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq;
|
|
69
|
+
trail.push({ seq, reason: 'aborted' });
|
|
70
|
+
return { reason: 'aborted', steps, endReasonTrail: trail };
|
|
71
|
+
}
|
|
72
|
+
steps++;
|
|
73
|
+
log.snapshotModelRequest(); // agent-loop 集成点:请求时快照(N1)
|
|
74
|
+
const step = await model();
|
|
75
|
+
if (step.stopReason === 'error') {
|
|
76
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'error' }).seq;
|
|
77
|
+
trail.push({ seq, reason: 'error' });
|
|
78
|
+
return { reason: 'error', steps, endReasonTrail: trail };
|
|
79
|
+
}
|
|
80
|
+
// ===== ADR-001:解析前收口 =====
|
|
81
|
+
if (step.stopReason === 'length') {
|
|
82
|
+
sawMaxTokens = true;
|
|
83
|
+
const readonlyRetry = preset.maxTokensRetryReadOnly === true;
|
|
84
|
+
const trunc = step.truncatedTools ?? [];
|
|
85
|
+
const retryable = readonlyRetry && trunc.every(t => tools.get(t.tool)?.declaredSideEffect === 'readonly');
|
|
86
|
+
if (retryable && trunc.length) {
|
|
87
|
+
// Pi 式补错重试:只读白名单工具收到「参数可能被截断」错误结果,交回模型下轮
|
|
88
|
+
for (const t of trunc) {
|
|
89
|
+
log.append('runtime', 'toolCall', turnId, { id: t.id, tool: t.tool, args: {}, truncated: true });
|
|
90
|
+
log.append('runtime', 'toolResult', turnId, { id: t.id, error: 'arguments may be truncated (max-tokens)' });
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// 默认收口:截断响应不解析、不执行任何工具调用
|
|
95
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'max-tokens' }).seq;
|
|
96
|
+
trail.push({ seq, reason: 'max-tokens' });
|
|
97
|
+
return { reason: 'max-tokens', steps, endReasonTrail: trail };
|
|
98
|
+
}
|
|
99
|
+
if (step.stopReason === 'toolUse' && step.toolCalls?.length) {
|
|
100
|
+
const batch = await executeBatch(step.toolCalls);
|
|
101
|
+
// F13:授权拒绝即收口(blockOnDeny 产出点——blocked 仅新增产出点,枚举不增)
|
|
102
|
+
if (preset.blockOnDeny && batch.denies > 0) {
|
|
103
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'blocked', denies: batch.denies }).seq;
|
|
104
|
+
trail.push({ seq, reason: 'blocked' });
|
|
105
|
+
return { reason: 'blocked', steps, endReasonTrail: trail };
|
|
106
|
+
}
|
|
107
|
+
const agg = preset.aggregate ?? {};
|
|
108
|
+
const abortedNow = (agg.terminate ?? 'all') === 'any' ? batch.terminateVotes > 0 : batch.terminateAll;
|
|
109
|
+
if (abortedNow) { // terminate 收口:默认 AND(整批),可配置 any
|
|
110
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq;
|
|
111
|
+
trail.push({ seq, reason: 'aborted' });
|
|
112
|
+
return { reason: 'aborted', steps, endReasonTrail: trail };
|
|
113
|
+
}
|
|
114
|
+
const concludedNow = (agg.concludesTurn ?? 'any') === 'any' ? batch.concludesTurn : batch.concludesAll && batch.finalized > 0;
|
|
115
|
+
if (concludedNow) { // concludesTurn 收口:默认 OR,可配置 all
|
|
116
|
+
const reason = sawMaxTokens ? 'max-tokens' : 'completed';
|
|
117
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason }).seq;
|
|
118
|
+
trail.push({ seq, reason });
|
|
119
|
+
return { reason, steps, endReasonTrail: trail };
|
|
120
|
+
}
|
|
121
|
+
// null:工具结果交回模型,继续下一 step(非异常)
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// stopReason === 'stop':模型自然完成
|
|
125
|
+
const reason = sawMaxTokens ? 'max-tokens' : 'completed';
|
|
126
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason }).seq;
|
|
127
|
+
trail.push({ seq, reason });
|
|
128
|
+
return { reason, steps, endReasonTrail: trail };
|
|
129
|
+
}
|
|
130
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'error', detail: 'max-steps' }).seq;
|
|
131
|
+
trail.push({ seq, reason: 'error' });
|
|
132
|
+
return { reason: 'error', steps, endReasonTrail: trail };
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
if (e instanceof TurnAborted) {
|
|
136
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq;
|
|
137
|
+
trail.push({ seq, reason: 'aborted' });
|
|
138
|
+
return { reason: 'aborted', steps, endReasonTrail: trail };
|
|
139
|
+
}
|
|
140
|
+
// 1.4-S4:CarM8Error 保形——code/userHint/retryable 进 turnEnd(A080001 引导文案「请运行
|
|
141
|
+
// car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变)
|
|
142
|
+
const m8 = e instanceof CarM8Error;
|
|
143
|
+
const detail = m8 ? `${e.slug}: ${e.message}` : String(e);
|
|
144
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, {
|
|
145
|
+
reason: 'error', detail,
|
|
146
|
+
...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable } : {}),
|
|
147
|
+
}).seq;
|
|
148
|
+
trail.push({ seq, reason: 'error' });
|
|
149
|
+
return { reason: 'error', steps, endReasonTrail: trail };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F8 · MCP 桥接网关:登记制 + 工具桥接 + 崩溃隔离 + 凭据门
|
|
3
|
+
*
|
|
4
|
+
* 口径(《系统设计》M5 / 冻结决策① / 安全设计 T3 域):
|
|
5
|
+
* - serverId 登记制:运行期不接受未登记连接(A050001);Python 侧仅工具供给方,无 ctx 级 API
|
|
6
|
+
* - 工具桥接为一等 ToolDefinition:declaredSideEffect 未声明按 write 最高约束(T-22)
|
|
7
|
+
* - 调用过三权限审批门(与其他工具同一审批门,无旁路——US-6 AC3)
|
|
8
|
+
* - Server 崩溃/超时 → BD-02 整体标记不可用,不传导进内核主链路(US-6 AC4)
|
|
9
|
+
* - McpServerConfig.env 禁明文密钥(疑似凭据模式即拒绝,sk- 前缀等——§3.2.5 传参禁忌)
|
|
10
|
+
* - 传输抽象:stdio 真实通道(JSON-RPC over 换行分隔 JSON)/ in-process 测试通道
|
|
11
|
+
*
|
|
12
|
+
* 1.8(D-29 W3C traceparent client 侧注入):callTool 加法 opts.traceparent → params._meta
|
|
13
|
+
* (MCP `_meta` 保留扩展点——未知键对端忽略,加法无害);仅 otel 显式开且 client span 存活时
|
|
14
|
+
* 非空(trace.ts mcpToolSpan)——**缺省不注入,JSON-RPC 字节面不变**;tools/list 不注入
|
|
15
|
+
* (注册期无 span 上下文,登记)。解析/校验归 trace.ts(parseTraceparent fail-open),本层原样透传。
|
|
16
|
+
*
|
|
17
|
+
* 1.9(D-32 远程 MCP 头通道):callTool 把 opts.traceparent 加法透传给 transport.send 第二参——
|
|
18
|
+
* 载体由传输层自选(HTTP 通道注入 `traceparent` 请求头〔W3C Trace Context 惯例〕;stdio 通道
|
|
19
|
+
* 忽略 opts,`params._meta` 载体不变)。`_meta` 注入口径一字节不动——otel 开时 HTTP 请求
|
|
20
|
+
* 双载体同值(头为主消费面,登记);otel 关时双载体零注入(头面与字节面不变)。
|
|
21
|
+
*/
|
|
22
|
+
export interface JsonRpcRequest {
|
|
23
|
+
jsonrpc: '2.0';
|
|
24
|
+
id: number;
|
|
25
|
+
method: string;
|
|
26
|
+
params?: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface JsonRpcResponse {
|
|
29
|
+
jsonrpc: '2.0';
|
|
30
|
+
id: number;
|
|
31
|
+
result?: unknown;
|
|
32
|
+
error?: {
|
|
33
|
+
code: number;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 客户端传输(CAR → MCP Server 方向)。M3-S11 双向分层:本接口为 client 侧对偶;
|
|
39
|
+
* server 侧(宿主 → CAR)见 src/host/hostGateway.ts 的 ServerTransport。
|
|
40
|
+
* 1.9-D32:send 加法可选第二参 opts(既有 `send(req)` 实现结构兼容零改动)。
|
|
41
|
+
*/
|
|
42
|
+
export interface ClientTransport {
|
|
43
|
+
send(req: JsonRpcRequest, opts?: {
|
|
44
|
+
traceparent?: string;
|
|
45
|
+
}): Promise<JsonRpcResponse>;
|
|
46
|
+
/** 进程/通道存活状态 */
|
|
47
|
+
alive(): boolean;
|
|
48
|
+
/** 主动终止 */
|
|
49
|
+
close(): void;
|
|
50
|
+
}
|
|
51
|
+
/** M1 兼容别名(M2 代码零改动;M4 评估移除) */
|
|
52
|
+
export type McpTransport = ClientTransport;
|
|
53
|
+
/**
|
|
54
|
+
* 服务端传输对偶(宿主 → CAR 方向,M3 多宿主):宿主作为 MCP client 调用 CAR 暴露的 tool 面。
|
|
55
|
+
* 与 ClientTransport 语义对偶:CAR 不主动 send,仅响应 method 调用。
|
|
56
|
+
*/
|
|
57
|
+
export interface ServerTransport {
|
|
58
|
+
/** 宿主到达的 JSON-RPC 方法调用(由 HostGateway 分发到 10 tool 注册表) */
|
|
59
|
+
onRequest(method: string, params: unknown): Promise<JsonRpcResponse>;
|
|
60
|
+
alive(): boolean;
|
|
61
|
+
close(): void;
|
|
62
|
+
}
|
|
63
|
+
/** 疑似明文凭据检测(§3.2.5:env 值出现 sk- 等前缀即拒绝) */
|
|
64
|
+
export declare function containsPlaintextCredential(env: Record<string, string>): string | null;
|
|
65
|
+
export interface McpServerConfig {
|
|
66
|
+
serverId: string;
|
|
67
|
+
transport: ClientTransport;
|
|
68
|
+
/** 启动环境变量(禁明文凭据,凭据经 M8 凭据门运行时注入) */
|
|
69
|
+
env?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
export interface McpToolDefinition {
|
|
72
|
+
serverId: string;
|
|
73
|
+
name: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
/** 未声明按 write 最高约束(T-22:安全默认) */
|
|
76
|
+
declaredSideEffect?: 'readonly' | 'write';
|
|
77
|
+
/** 工具入参 JSON Schema(MCP tools/list 原样捕获,M5 DEC-4 深度参数渲染消费源) */
|
|
78
|
+
inputSchema?: unknown;
|
|
79
|
+
}
|
|
80
|
+
export declare class McpGateway {
|
|
81
|
+
#private;
|
|
82
|
+
/** 登记并连接:拉起后发现工具并注册进能力矩阵(加载报告可见) */
|
|
83
|
+
register(cfg: McpServerConfig): Promise<McpToolDefinition[]>;
|
|
84
|
+
/** 能力矩阵视图(加载报告用) */
|
|
85
|
+
listTools(): McpToolDefinition[];
|
|
86
|
+
isAvailable(serverId: string): boolean;
|
|
87
|
+
/** 调用:走统一权限/审计链路(无旁路);结果回填日志前必经 redact(调用方职责) */
|
|
88
|
+
callTool(serverId: string, tool: string, args: Record<string, unknown>, opts?: {
|
|
89
|
+
timeoutMs?: number; /** 1.8-D29:W3C traceparent(otel 显式开且 client span 存活时非空;缺省不注入 _meta) */
|
|
90
|
+
traceparent?: string;
|
|
91
|
+
}): Promise<{
|
|
92
|
+
ok: boolean;
|
|
93
|
+
result?: unknown;
|
|
94
|
+
error?: string;
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** 疑似明文凭据检测(§3.2.5:env 值出现 sk- 等前缀即拒绝) */
|
|
2
|
+
export function containsPlaintextCredential(env) {
|
|
3
|
+
const patterns = [/^sk-/, /^ghp_/, /^xox[bap]-/, /^AKIA/];
|
|
4
|
+
for (const [k, v] of Object.entries(env)) {
|
|
5
|
+
if (patterns.some(p => p.test(v)))
|
|
6
|
+
return `env "${k}" looks like a plaintext credential(经 M8 凭据门注入,禁止明文透传)`;
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
export class McpGateway {
|
|
11
|
+
#servers = new Map();
|
|
12
|
+
#tools = new Map();
|
|
13
|
+
#unavailable = new Set();
|
|
14
|
+
#nextId = 1;
|
|
15
|
+
/** 登记并连接:拉起后发现工具并注册进能力矩阵(加载报告可见) */
|
|
16
|
+
async register(cfg) {
|
|
17
|
+
if (this.#servers.has(cfg.serverId)) {
|
|
18
|
+
throw new Error(`CAR-E-MCP: serverId "${cfg.serverId}" already registered(登记制:重复注册显式报错)`);
|
|
19
|
+
}
|
|
20
|
+
const cred = cfg.env ? containsPlaintextCredential(cfg.env) : null;
|
|
21
|
+
if (cred)
|
|
22
|
+
throw new Error(`CAR-E-MCP: ${cred}`);
|
|
23
|
+
this.#servers.set(cfg.serverId, cfg);
|
|
24
|
+
const res = await cfg.transport.send({ jsonrpc: '2.0', id: this.#nextId++, method: 'tools/list' });
|
|
25
|
+
const tools = res.result.tools;
|
|
26
|
+
for (const t of tools) {
|
|
27
|
+
// T-22:未声明 sideEffect 按 write 最高约束收敛(安全默认,注册时强制)
|
|
28
|
+
this.#tools.set(`${cfg.serverId}:${t.name}`, {
|
|
29
|
+
serverId: cfg.serverId, name: t.name, description: t.description,
|
|
30
|
+
declaredSideEffect: t.sideEffect ?? 'write',
|
|
31
|
+
inputSchema: t.inputSchema,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return [...this.#tools.values()].filter(t => t.serverId === cfg.serverId);
|
|
35
|
+
}
|
|
36
|
+
/** 能力矩阵视图(加载报告用) */
|
|
37
|
+
listTools() { return [...this.#tools.values()]; }
|
|
38
|
+
isAvailable(serverId) { return this.#servers.has(serverId) && !this.#unavailable.has(serverId); }
|
|
39
|
+
/** 调用:走统一权限/审计链路(无旁路);结果回填日志前必经 redact(调用方职责) */
|
|
40
|
+
async callTool(serverId, tool, args, opts = {}) {
|
|
41
|
+
const cfg = this.#servers.get(serverId);
|
|
42
|
+
if (!cfg)
|
|
43
|
+
throw new Error('CAR-A050001: unregistered MCP connection rejected(登记制)');
|
|
44
|
+
if (this.#unavailable.has(serverId)) {
|
|
45
|
+
// BD-02:崩溃/超时整体标记不可用——显式错误结果,不抛异常不阻断主链路
|
|
46
|
+
return { ok: false, error: `MCP server "${serverId}" unavailable (BD-02)` };
|
|
47
|
+
}
|
|
48
|
+
const key = `${serverId}:${tool}`;
|
|
49
|
+
if (!this.#tools.has(key))
|
|
50
|
+
return { ok: false, error: `unknown tool "${tool}" on "${serverId}"` };
|
|
51
|
+
try {
|
|
52
|
+
const res = await Promise.race([
|
|
53
|
+
// 1.9-D32:traceparent 加法透传第二参(载体归传输层——HTTP 头 / stdio 忽略);
|
|
54
|
+
// `_meta` 注入 1.8-D29 口径不动(otel 开 = 双载体同值)
|
|
55
|
+
cfg.transport.send({
|
|
56
|
+
jsonrpc: '2.0', id: this.#nextId++, method: 'tools/call',
|
|
57
|
+
params: { name: tool, arguments: args, ...(opts.traceparent ? { _meta: { traceparent: opts.traceparent } } : {}) },
|
|
58
|
+
}, opts.traceparent ? { traceparent: opts.traceparent } : undefined),
|
|
59
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('mcp timeout')), opts.timeoutMs ?? 30_000)),
|
|
60
|
+
]);
|
|
61
|
+
return { ok: true, result: res.result };
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
// BD-02:崩溃/超时 → 整体不可用(不静默:错误事件由调用方落审计日志)
|
|
65
|
+
this.#unavailable.add(serverId);
|
|
66
|
+
return { ok: false, error: `MCP call failed, server marked unavailable (BD-02): ${String(e)}` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ClientTransport } from './gateway.ts';
|
|
2
|
+
export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
3
|
+
export interface HttpMcpServerSpec {
|
|
4
|
+
url: string;
|
|
5
|
+
/** 已展开的请求头(明文禁令与 ${ENV} 展开由 resolveMcpHeaders 承担——直接传入者自证合规) */
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
/** 测试注入;缺省 30s(send 内建超时,与 stdio 同口径) */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** node:http(s) 响应的最小面(真实 res 兼容;测试可注入替身) */
|
|
11
|
+
export interface RawHttpResponse {
|
|
12
|
+
status: number;
|
|
13
|
+
headers: Record<string, string | string[] | undefined>;
|
|
14
|
+
/** 流式响应体(node:http res 本身即 AsyncIterable<Uint8Array>;destroy 显式丢弃) */
|
|
15
|
+
body: AsyncIterable<Uint8Array> & {
|
|
16
|
+
destroy(): void;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** node:http(s) POST 单发(1.4-BUG-2 口径:出站不走 undici;TLS 门由 createHttpMcpTransport 把守) */
|
|
20
|
+
export declare function rawPost(url: string, headers: Record<string, string>, body: string, timeoutMs: number): Promise<RawHttpResponse>;
|
|
21
|
+
/**
|
|
22
|
+
* ${ENV_VAR} 头引用展开(D-21):值内任意位置的 `${NAME}` 均替换为 env.NAME(如
|
|
23
|
+
* `Bearer ${TOKEN}`);env 缺席显式报错(fail-visible,禁静默空值);字面值疑似明文凭据
|
|
24
|
+
* 直接拒绝(按原始值检查——展开值是凭据本身,引注入即目的)。
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveMcpHeaders(raw: Record<string, string>, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
27
|
+
export declare function createHttpMcpTransport(spec: HttpMcpServerSpec): ClientTransport;
|