@kasenri/dsh-orbit 0.5.6 → 0.5.8
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 +24 -19
- package/cordis.patch.yml +2 -2
- package/lib/activation.js +9 -9
- package/lib/capabilities.js +23 -0
- package/lib/client.js +49 -79
- package/lib/decisions.js +4 -4
- package/lib/dsh-host.js +108 -30
- package/lib/evidence.js +24 -0
- package/lib/guard.js +17 -17
- package/lib/index.js +44 -42
- package/lib/kernel.js +33 -21
- package/lib/pipeline-guard.js +16 -4
- package/lib/routes.js +67 -37
- package/lib/sanitize.js +3 -2
- package/lib/service.js +75 -15
- package/lib/settlement.js +4 -1
- package/lib/state-store.js +15 -7
- package/lib/supervisor.js +197 -91
- package/lib/tool.js +10 -11
- package/lib/types.js +6 -5
- package/package.json +1 -1
package/lib/dsh-host.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
5
|
import { collectTurnToolFacts } from "./evidence.js";
|
|
3
6
|
import { redactText, truncateSafe } from "./sanitize.js";
|
|
4
7
|
import { classifyTurnSettlement } from "./settlement.js";
|
|
@@ -24,6 +27,7 @@ export class DshOrbitHost {
|
|
|
24
27
|
ownedChildren = new Set();
|
|
25
28
|
interruptedChildren = new Set();
|
|
26
29
|
childParents = new Map();
|
|
30
|
+
childGrants = new Map();
|
|
27
31
|
nowFn;
|
|
28
32
|
sleepFn;
|
|
29
33
|
constructor(ctx, options = {}) {
|
|
@@ -39,12 +43,14 @@ export class DshOrbitHost {
|
|
|
39
43
|
}
|
|
40
44
|
async startRole(request) {
|
|
41
45
|
const parent = this.parent();
|
|
46
|
+
if (request.workspace !== undefined && resolve(parent.session.header.cwd ?? process.cwd()) !== resolve(request.workspace)) {
|
|
47
|
+
throw new Error('ORBIT_WORKSPACE_MISMATCH: 子代理工作目录必须与 Orbit 持有的 workspace 一致。');
|
|
48
|
+
}
|
|
42
49
|
const prompt = [{ type: 'text', text: request.prompt }];
|
|
43
50
|
const agentOptions = {
|
|
44
51
|
provider: request.route.provider,
|
|
45
52
|
model: request.route.model,
|
|
46
|
-
|
|
47
|
-
...(request.route.maxTokens ? { maxTokens: request.route.maxTokens } : {}),
|
|
53
|
+
reasoningEffort: request.route.reasoningEffort,
|
|
48
54
|
};
|
|
49
55
|
if (request.role === 'executor') {
|
|
50
56
|
return this.startExecutor(parent, request, prompt, agentOptions);
|
|
@@ -70,7 +76,7 @@ export class DshOrbitHost {
|
|
|
70
76
|
...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
|
|
71
77
|
...(request.outputSchema ? { outputSchema: request.outputSchema } : {}),
|
|
72
78
|
}));
|
|
73
|
-
this.registerChild(run.id, parent);
|
|
79
|
+
this.registerChild(run.id, parent, request);
|
|
74
80
|
const result = run.result
|
|
75
81
|
.then((value) => ({
|
|
76
82
|
childId: run.id,
|
|
@@ -106,13 +112,18 @@ export class DshOrbitHost {
|
|
|
106
112
|
if (request.resumeOf) {
|
|
107
113
|
const existingId = request.resumeOf;
|
|
108
114
|
const existing = this.ctx.agents.get(existingId);
|
|
109
|
-
|
|
115
|
+
const grant = this.childGrants.get(existingId);
|
|
116
|
+
const requestedTools = new Set(request.toolFilter?.allow ?? []);
|
|
117
|
+
const sameGrant = grant?.role === 'executor' && grant.workspace === (request.workspace ?? parent.session.header.cwd ?? process.cwd())
|
|
118
|
+
&& grant.tools.size === requestedTools.size && [...grant.tools].every((tool) => requestedTools.has(tool));
|
|
119
|
+
if (existing && sameGrant && this.childParents.get(existingId) === parent && this.ctx.agents.isOwnedBy(SessionId(existingId), parent)) {
|
|
110
120
|
const agent = existing;
|
|
111
121
|
this.interruptedChildren.delete(existingId);
|
|
112
|
-
const
|
|
122
|
+
const previousTurns = (agent.session?.snapshotEvents?.() ?? []).filter((event) => event.type === 'turn/start').length;
|
|
113
123
|
await this.ctx.subagents.sendMessage(parent, existingId, prompt, {
|
|
114
|
-
signal: new AbortController().signal,
|
|
124
|
+
signal: request.signal ?? new AbortController().signal,
|
|
115
125
|
});
|
|
126
|
+
const done = this.waitForExecutorSettlement(agent, existingId, previousTurns);
|
|
116
127
|
return {
|
|
117
128
|
childId: existingId,
|
|
118
129
|
result: done,
|
|
@@ -124,19 +135,28 @@ export class DshOrbitHost {
|
|
|
124
135
|
}
|
|
125
136
|
const controller = new AbortController();
|
|
126
137
|
request.signal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const reservedId = SessionId(randomUUID());
|
|
139
|
+
const childId = String(reservedId);
|
|
140
|
+
this.registerChild(childId, parent, request);
|
|
141
|
+
let started;
|
|
142
|
+
try {
|
|
143
|
+
started = await this.ctx.subagents.startContinuable({
|
|
144
|
+
provider: 'spawn',
|
|
145
|
+
label: request.label,
|
|
146
|
+
childId: reservedId,
|
|
147
|
+
request: {
|
|
148
|
+
prompt,
|
|
149
|
+
parent,
|
|
150
|
+
agentOptions,
|
|
151
|
+
...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
|
|
152
|
+
},
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
this.forgetChild(childId);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
140
160
|
const agent = this.ctx.agents.get(started.childId);
|
|
141
161
|
const result = agent
|
|
142
162
|
? this.waitForExecutorSettlement(agent, childId)
|
|
@@ -149,19 +169,26 @@ export class DshOrbitHost {
|
|
|
149
169
|
runtimeSnapshot: agent ? () => this.snapshotAgent(agent) : undefined,
|
|
150
170
|
};
|
|
151
171
|
}
|
|
152
|
-
registerChild(childId, parent) {
|
|
172
|
+
registerChild(childId, parent, request) {
|
|
153
173
|
this.ownedChildren.add(childId);
|
|
154
174
|
this.childParents.set(childId, parent);
|
|
175
|
+
this.childGrants.set(childId, {
|
|
176
|
+
role: request.role,
|
|
177
|
+
workspace: resolve(request.workspace ?? parent.session.header.cwd ?? process.cwd()),
|
|
178
|
+
tools: new Set(request.toolFilter?.allow ?? []),
|
|
179
|
+
});
|
|
155
180
|
}
|
|
156
181
|
forgetChild(childId) {
|
|
157
182
|
this.ownedChildren.delete(childId);
|
|
158
183
|
this.interruptedChildren.delete(childId);
|
|
159
184
|
this.childParents.delete(childId);
|
|
185
|
+
this.childGrants.delete(childId);
|
|
160
186
|
}
|
|
161
187
|
async interruptExecutor(childId, reason) {
|
|
162
188
|
this.interruptedChildren.add(childId);
|
|
163
189
|
const parent = this.childParents.get(childId);
|
|
164
190
|
this.ctx.subagents.interrupt(childId, parent ? { kind: 'ancestor', agent: parent } : { kind: 'user', parentSessionId: childId });
|
|
191
|
+
await this.ctx.agents.get(SessionId(childId))?.whenIdle();
|
|
165
192
|
void reason;
|
|
166
193
|
}
|
|
167
194
|
async drainExecutor(parent, childId) {
|
|
@@ -173,8 +200,8 @@ export class DshOrbitHost {
|
|
|
173
200
|
}
|
|
174
201
|
this.forgetChild(childId);
|
|
175
202
|
}
|
|
176
|
-
async waitForExecutorSettlement(agent, childId) {
|
|
177
|
-
await this.waitForTurnOrIdle(agent);
|
|
203
|
+
async waitForExecutorSettlement(agent, childId, previousTurns = 0) {
|
|
204
|
+
await this.waitForTurnOrIdle(agent, previousTurns);
|
|
178
205
|
const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
|
|
179
206
|
const classified = classifyTurnSettlement(events);
|
|
180
207
|
const output = this.readFinalOutput(agent);
|
|
@@ -214,20 +241,22 @@ export class DshOrbitHost {
|
|
|
214
241
|
return { childId, output, interrupted: true, reason: 'EXECUTOR_BLOCKED', telemetry, ...evidence };
|
|
215
242
|
case 'max-tokens':
|
|
216
243
|
return { childId, output, interrupted: true, reason: 'EXECUTOR_MAX_TOKENS', telemetry, ...evidence };
|
|
244
|
+
case 'interrupted':
|
|
245
|
+
return { childId, output, interrupted: true, reason: 'EXECUTOR_INTERRUPTED', telemetry, ...evidence };
|
|
217
246
|
default:
|
|
218
247
|
return { childId, output, interrupted: true, reason: 'EXECUTOR_NO_TURN', telemetry, ...evidence };
|
|
219
248
|
}
|
|
220
249
|
}
|
|
221
|
-
async waitForTurnOrIdle(agent) {
|
|
250
|
+
async waitForTurnOrIdle(agent, previousTurns = 0) {
|
|
222
251
|
const deadline = this.nowFn() + EXECUTOR_TURN_START_TIMEOUT_MS;
|
|
223
|
-
while (this.nowFn() < deadline && !this.hasTurnStarted(agent)) {
|
|
252
|
+
while (this.nowFn() < deadline && !this.hasTurnStarted(agent, previousTurns)) {
|
|
224
253
|
await this.sleepFn(20);
|
|
225
254
|
}
|
|
226
255
|
await agent.whenIdle?.();
|
|
227
256
|
}
|
|
228
|
-
hasTurnStarted(agent) {
|
|
257
|
+
hasTurnStarted(agent, previousTurns) {
|
|
229
258
|
const events = agent.session?.snapshotEvents?.() ?? [];
|
|
230
|
-
return events.
|
|
259
|
+
return events.filter((event) => event.type === 'turn/start').length > previousTurns;
|
|
231
260
|
}
|
|
232
261
|
readFinalOutput(agent) {
|
|
233
262
|
const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
|
|
@@ -308,6 +337,53 @@ export class DshOrbitHost {
|
|
|
308
337
|
const agent = this.ctx.agents.currentInitiator();
|
|
309
338
|
return this.ctx.tools.get(name, agent) !== undefined;
|
|
310
339
|
}
|
|
340
|
+
async validateRoutes(routes, signal) {
|
|
341
|
+
const issues = [];
|
|
342
|
+
const labels = { commander: '指挥官', executor: '执行员', watchdog: '监控模型' };
|
|
343
|
+
for (const role of ['commander', 'executor', 'watchdog']) {
|
|
344
|
+
const route = routes[role];
|
|
345
|
+
try {
|
|
346
|
+
const llm = this.ctx.reflect.get('llm');
|
|
347
|
+
if (!llm)
|
|
348
|
+
throw new Error('DSH LLM registry 不可用');
|
|
349
|
+
const deadline = AbortSignal.timeout(10_000);
|
|
350
|
+
const activeSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
351
|
+
const info = await llm.resolveModelInfo(route.provider, route.model, activeSignal);
|
|
352
|
+
const catalog = await llm.listModels(route.provider);
|
|
353
|
+
if (catalog.length > 0 && !catalog.some((entry) => entry.id === route.model)) {
|
|
354
|
+
issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前模型目录中不存在,请重新选择。`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (route.reasoningEffort !== undefined) {
|
|
358
|
+
const efforts = info.reasoning?.efforts ?? [];
|
|
359
|
+
if (!efforts.some((effort) => effort.id === route.reasoningEffort)) {
|
|
360
|
+
issues.push(`${labels[role]}:ORBIT_REASONING_EFFORT_UNAVAILABLE (${route.provider}/${route.model}/${route.reasoningEffort}),当前模型未声明此推理等级。`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前不可用,请重新选择:${truncateSafe(error instanceof Error ? error.message : String(error), 200)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return issues;
|
|
369
|
+
}
|
|
370
|
+
isMutationAuthorized(agent, cwd, tool) {
|
|
371
|
+
const id = String(agent?.id ?? '');
|
|
372
|
+
const grant = this.childGrants.get(id);
|
|
373
|
+
const parent = this.childParents.get(id);
|
|
374
|
+
return grant?.role === 'executor' && grant.workspace === resolve(cwd) && grant.tools.has(tool)
|
|
375
|
+
&& !this.interruptedChildren.has(id) && this.ctx.agents.get(SessionId(id)) === agent
|
|
376
|
+
&& parent !== undefined && this.ctx.agents.isOwnedBy(SessionId(id), parent);
|
|
377
|
+
}
|
|
378
|
+
async revokeWorkspace(cwd) {
|
|
379
|
+
const ids = [...this.childGrants].filter(([, grant]) => grant.workspace === resolve(cwd) && grant.role === 'executor').map(([id]) => id);
|
|
380
|
+
for (const id of ids) {
|
|
381
|
+
const parent = this.childParents.get(id);
|
|
382
|
+
await this.interruptExecutor(id, 'ORBIT_WORKSPACE_RELEASED');
|
|
383
|
+
if (parent)
|
|
384
|
+
await this.drainExecutor(parent, id);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
311
387
|
/**
|
|
312
388
|
* Mutation ownership is about top-level autonomous drivers, not about every
|
|
313
389
|
* running agent. The calling parent, Orbit's own children, and ordinary
|
|
@@ -316,17 +392,19 @@ export class DshOrbitHost {
|
|
|
316
392
|
* at tool start by the Orbit mutation guard.
|
|
317
393
|
*/
|
|
318
394
|
async otherMutationDrivers(cwd) {
|
|
319
|
-
void cwd;
|
|
320
395
|
const drivers = [];
|
|
321
396
|
const initiator = this.ctx.agents.currentInitiator();
|
|
322
397
|
// `ctx.reflect.get` is the official service lookup that does not require an
|
|
323
398
|
// inject declaration, so Orbit stays loadable in profiles without dsh-goal.
|
|
324
399
|
const reflect = this.ctx.reflect;
|
|
325
400
|
const goals = reflect?.get('goals');
|
|
326
|
-
|
|
401
|
+
const agents = this.ctx.agents.list?.() ?? (initiator ? [initiator] : []);
|
|
402
|
+
for (const candidate of agents) {
|
|
403
|
+
if (!goals || resolve(candidate.session.header.cwd ?? process.cwd()) !== resolve(cwd) || this.ownedChildren.has(String(candidate.id)))
|
|
404
|
+
continue;
|
|
327
405
|
try {
|
|
328
|
-
const goal = goals.get(
|
|
329
|
-
if (goal?.phase === 'active')
|
|
406
|
+
const goal = goals.get(candidate);
|
|
407
|
+
if (goal?.phase === 'active' && !drivers.includes('goal'))
|
|
330
408
|
drivers.push('goal');
|
|
331
409
|
}
|
|
332
410
|
catch {
|
package/lib/evidence.js
CHANGED
|
@@ -147,3 +147,27 @@ export function buildEvidenceBundle(input) {
|
|
|
147
147
|
export function formatEvidenceBundle(bundle) {
|
|
148
148
|
return bounded(JSON.stringify(bundle), EVIDENCE_LIMITS.total);
|
|
149
149
|
}
|
|
150
|
+
/** Persist only compact result facts, never a child transcript. */
|
|
151
|
+
export function buildStepResult(stepId, attempt, bundle, tests = []) {
|
|
152
|
+
return {
|
|
153
|
+
step_id: bounded(stepId, 80),
|
|
154
|
+
attempt,
|
|
155
|
+
summary: bounded(bundle.executor_summary ?? '', EVIDENCE_LIMITS.executorSummary),
|
|
156
|
+
changed_files: bundle.changed_files.map((entry) => bounded(entry, EVIDENCE_LIMITS.entry)).slice(0, EVIDENCE_LIMITS.changedFiles),
|
|
157
|
+
test_summary: [...tests, ...bundle.tests.map((entry) => `${entry.command}: ${entry.status}`)]
|
|
158
|
+
.slice(0, EVIDENCE_LIMITS.testEvidence).map((entry) => bounded(entry, EVIDENCE_LIMITS.entry)),
|
|
159
|
+
evidence: formatEvidenceBundle(bundle),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Fair per-step quotas keep every step visible in a bounded final review. */
|
|
163
|
+
export function formatStepResults(state) {
|
|
164
|
+
const steps = state.plan.steps;
|
|
165
|
+
const quota = Math.max(1, Math.floor(EVIDENCE_LIMITS.total / Math.max(1, steps.length)) - 100);
|
|
166
|
+
return steps.map((step) => {
|
|
167
|
+
const result = state.step_results?.find((entry) => entry.step_id === step.id);
|
|
168
|
+
const content = result === undefined ? '未记录持久化结果' : JSON.stringify({
|
|
169
|
+
summary: result.summary, test_summary: result.test_summary, changed_files: result.changed_files, evidence: result.evidence,
|
|
170
|
+
});
|
|
171
|
+
return `${bounded(step.id, 80)}[${step.status}] ${bounded(content, quota)}`;
|
|
172
|
+
}).join('\n');
|
|
173
|
+
}
|
package/lib/guard.js
CHANGED
|
@@ -10,23 +10,23 @@ const GITHUB_REMOTE_PATTERNS = [
|
|
|
10
10
|
/\bgit\s+remote\s+(?:set-url|add|remove)\b/i,
|
|
11
11
|
];
|
|
12
12
|
const DANGEROUS_PATTERNS = [
|
|
13
|
-
['destructive_operation', /\brm\s+-[A-Za-z]*r[A-Za-z]*f|\brm\s+-rf\b/i, '
|
|
14
|
-
['destructive_operation', /\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[A-Za-z]*f/i, '
|
|
15
|
-
['production_operation', /\bdrop\s+database\b|\b(?:drop|truncate)\s+(?:table|schema)\b/i, '
|
|
13
|
+
['destructive_operation', /\brm\s+-[A-Za-z]*r[A-Za-z]*f|\brm\s+-rf\b/i, '递归强制删除'],
|
|
14
|
+
['destructive_operation', /\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[A-Za-z]*f/i, '不可逆的 git reset/clean'],
|
|
15
|
+
['production_operation', /\bdrop\s+database\b|\b(?:drop|truncate)\s+(?:table|schema)\b/i, '不可逆的数据库操作'],
|
|
16
16
|
[
|
|
17
17
|
'production_operation',
|
|
18
18
|
/\b(?:production|prod)\b.*\b(?:deploy|migrate|database|restart)\b|\b(?:deploy|migrate)\b.*\b(?:production|prod)\b/i,
|
|
19
|
-
'
|
|
19
|
+
'生产环境操作',
|
|
20
20
|
],
|
|
21
21
|
[
|
|
22
22
|
'production_operation',
|
|
23
23
|
/\b(?:kubectl|helm)\s+(?:apply|delete|upgrade|rollback)\b|\bterraform\s+apply\b|\bdocker\s+push\b/i,
|
|
24
|
-
'
|
|
24
|
+
'部署或远程 registry 写入',
|
|
25
25
|
],
|
|
26
|
-
['production_operation', /\b(?:alembic|prisma|sequelize|rails)\b.*\b(?:upgrade|migrate|db:migrate)\b/i, '
|
|
27
|
-
['production_operation', /\b(?:ufw|iptables)\b.*\b(?:allow|insert|append)\b|\bdocker\s+run\b.*\s-p\s/i, '
|
|
28
|
-
['secret_operation', /\b(?:cat|less|more|head|tail)\b[^\n]*(?:\.env|secret|credential|auth\.json|cookie)/i, '
|
|
29
|
-
['secret_operation', /\b(?:curl|wget)\b[^\n]*(?:Authorization|Bearer|api[_-]?key|token)=?/i, '
|
|
26
|
+
['production_operation', /\b(?:alembic|prisma|sequelize|rails)\b.*\b(?:upgrade|migrate|db:migrate)\b/i, '数据库迁移'],
|
|
27
|
+
['production_operation', /\b(?:ufw|iptables)\b.*\b(?:allow|insert|append)\b|\bdocker\s+run\b.*\s-p\s/i, '暴露公共网络端口'],
|
|
28
|
+
['secret_operation', /\b(?:cat|less|more|head|tail)\b[^\n]*(?:\.env|secret|credential|auth\.json|cookie)/i, '输出凭证或敏感信息'],
|
|
29
|
+
['secret_operation', /\b(?:curl|wget)\b[^\n]*(?:Authorization|Bearer|api[_-]?key|token)=?/i, '携带凭证的网络请求'],
|
|
30
30
|
];
|
|
31
31
|
const SENSITIVE_ENV_NAME = /(?:^|_)(?:KEY|TOKEN|PASSWORD|PASSWD|SECRET|COOKIE|AUTH|CREDENTIAL|PRIVATE_KEY|ACCESS_TOKEN|REFRESH_TOKEN)(?:_|$)/i;
|
|
32
32
|
const DURABLE_STATE_PATH = /(?:^|[\s/'"])(?:\.\/)?\.cx(?:[/'"\s]|$)/i;
|
|
@@ -82,25 +82,25 @@ export function guardBashCommand(command, intent = {}) {
|
|
|
82
82
|
if (intent.github_allowed !== true) {
|
|
83
83
|
for (const pattern of GITHUB_REMOTE_PATTERNS) {
|
|
84
84
|
if (pattern.test(trimmed))
|
|
85
|
-
return block('github_remote_write',
|
|
85
|
+
return block('github_remote_write', `未授权 GitHub 远程写入:${pattern.source}`);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
if (isDurableStateWrite(trimmed)) {
|
|
89
|
-
return block('durable_state_write', '
|
|
89
|
+
return block('durable_state_write', '只有 Orbit controller 可以写入 .cx 持久状态。');
|
|
90
90
|
}
|
|
91
91
|
if (looksLikeEnvSecret(trimmed)) {
|
|
92
|
-
return block('secret_operation', '
|
|
92
|
+
return block('secret_operation', '不允许读取或导出敏感凭证。');
|
|
93
93
|
}
|
|
94
94
|
if (/^\s*export\s+[A-Za-z_][A-Za-z0-9_]*\s*=/i.test(trimmed)) {
|
|
95
95
|
const assignment = /^\s*export\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/i.exec(trimmed);
|
|
96
96
|
if (assignment && SENSITIVE_ENV_NAME.test(assignment[1] ?? '')) {
|
|
97
|
-
return block('secret_operation', '
|
|
97
|
+
return block('secret_operation', '不允许导出敏感环境变量。');
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
if (/^\s*set\s+[A-Za-z_][A-Za-z0-9_]*=/i.test(trimmed)) {
|
|
101
101
|
const assignment = /^\s*set\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/i.exec(trimmed);
|
|
102
102
|
if (assignment && SENSITIVE_ENV_NAME.test(assignment[1] ?? '')) {
|
|
103
|
-
return block('secret_operation', '
|
|
103
|
+
return block('secret_operation', '不允许设置敏感环境变量。');
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
for (const [code, pattern, reason] of DANGEROUS_PATTERNS) {
|
|
@@ -118,11 +118,11 @@ export function guardToolPath(toolName, targetPath, deps) {
|
|
|
118
118
|
const cwd = deps.cwd;
|
|
119
119
|
const absolute = isAbsolute(targetPath) ? targetPath : resolve(cwd, targetPath);
|
|
120
120
|
if (isInsideDurableState(absolute, cwd)) {
|
|
121
|
-
return block('durable_state_write', '
|
|
121
|
+
return block('durable_state_write', '只有 Orbit controller 可以写入 .cx 持久状态。');
|
|
122
122
|
}
|
|
123
123
|
const realpath = deps.realpath ?? safeRealpath;
|
|
124
124
|
if (isInsideDurableState(realpathWithin(absolute, realpath), cwd)) {
|
|
125
|
-
return block('durable_state_write', '
|
|
125
|
+
return block('durable_state_write', '只有 Orbit controller 可以写入 .cx 持久状态(已解析符号链接)。');
|
|
126
126
|
}
|
|
127
127
|
return { allowed: true };
|
|
128
128
|
}
|
|
@@ -153,5 +153,5 @@ function safeRealpath(path) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
export function guardReason(decision) {
|
|
156
|
-
return `
|
|
156
|
+
return `Orbit 安全护栏阻断当前调用 (${decision.code}):${decision.reason}`;
|
|
157
157
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import z from '@deepseek-ai/schemastery';
|
|
3
3
|
import { installOrbitGestureBoundary, registerOrbitCommand, registerOrbitToggleCommand } from "./activation.js";
|
|
4
|
-
import { estimateLoopCount } from "./kernel.js";
|
|
5
4
|
import { createOrbitPreExecuteHandler } from "./pipeline-guard.js";
|
|
6
|
-
import { resolveEffectiveRoutes,
|
|
5
|
+
import { agentDefaultSelectionOf, resolveEffectiveRoutes, sessionModelSelectionOf, sessionModelStateOf } from "./routes.js";
|
|
7
6
|
import { installOrbitSessionProjection, orbitEnabledOf } from "./session-state.js";
|
|
8
7
|
import { OrbitService } from "./service.js";
|
|
9
8
|
import { createOrbitTool } from "./tool.js";
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* must grant a budget that can finish a full plan; the goal complexity
|
|
14
|
-
* estimate raises it further when the goal asks for more.
|
|
10
|
+
* Fixed hard-activation budget. Explicit tool calls may still supply a bounded
|
|
11
|
+
* user-owned budget; ordinary activation never guesses complexity from words.
|
|
15
12
|
*/
|
|
16
|
-
const
|
|
13
|
+
const HARD_ACTIVATION_LOOP_BUDGET = 5;
|
|
17
14
|
export const name = 'dsh-orbit';
|
|
18
15
|
export const inject = ['tools', 'agents', 'subagents'];
|
|
19
16
|
const Route = z.object({
|
|
@@ -39,61 +36,60 @@ export const Config = z.object({
|
|
|
39
36
|
executor: Route,
|
|
40
37
|
watchdog: Route,
|
|
41
38
|
})
|
|
42
|
-
.default({
|
|
43
|
-
commander: { provider: 'deepseek-official', model: 'deepseek-v4-pro', reasoningEffort: 'high' },
|
|
44
|
-
executor: { provider: 'deepseek-official', model: 'deepseek-v4-flash', reasoningEffort: 'high' },
|
|
45
|
-
watchdog: { provider: 'deepseek-official', model: 'deepseek-v4-flash', reasoningEffort: 'low' },
|
|
46
|
-
}),
|
|
39
|
+
.default({}),
|
|
47
40
|
browserTools: z.array(z.string()).default(['agent_browser']),
|
|
48
41
|
commanderReadOnlyTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep', 'web_search', 'web_fetch']),
|
|
49
42
|
watchdogTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep']),
|
|
50
43
|
executorTools: z
|
|
51
44
|
.array(z.string())
|
|
52
|
-
.default(['read', 'read_image', 'glob', 'grep'
|
|
45
|
+
.default(['read', 'read_image', 'glob', 'grep']),
|
|
53
46
|
executorTimeoutMs: z.natural().default(480_000),
|
|
54
47
|
registerTool: z.boolean().default(true),
|
|
55
48
|
registerGuards: z.boolean().default(true),
|
|
56
49
|
slashCommand: z.boolean().default(true),
|
|
57
50
|
});
|
|
58
51
|
export function apply(ctx, config) {
|
|
59
|
-
// Orbit settings bridge
|
|
60
|
-
// routes
|
|
61
|
-
|
|
62
|
-
// without a settings provider keep running from `config.routes`.
|
|
63
|
-
let routeSettings;
|
|
52
|
+
// Orbit settings bridge. The package ships no model defaults; only explicit
|
|
53
|
+
// profile routes can act as a headless compatibility fallback.
|
|
54
|
+
let readRouteSettings = () => undefined;
|
|
64
55
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
65
|
-
const baseRoute = (route) => ({
|
|
66
|
-
provider: route.provider,
|
|
67
|
-
model: route.model,
|
|
68
|
-
reasoningEffort: route.reasoningEffort ?? '',
|
|
69
|
-
});
|
|
70
56
|
const scope = settingsCtx.settings.register('orbit', OrbitRouteSettingsSchema, {
|
|
71
57
|
base: {
|
|
72
|
-
commander:
|
|
73
|
-
watchdog:
|
|
58
|
+
commander: { provider: '', model: '', reasoningEffort: '' },
|
|
59
|
+
watchdog: { provider: '', model: '', reasoningEffort: '' },
|
|
74
60
|
},
|
|
75
61
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
79
|
-
sync();
|
|
80
|
-
scope.watch(() => {
|
|
81
|
-
sync();
|
|
82
|
-
});
|
|
62
|
+
readRouteSettings = () => scope.get();
|
|
63
|
+
settingsCtx.effect(() => () => { readRouteSettings = () => undefined; });
|
|
83
64
|
});
|
|
84
65
|
// A NEW run resolves its three routes exactly once: Commander/Watchdog from
|
|
85
66
|
// the Orbit settings (base = config), Executor from the initiating Session's
|
|
86
|
-
// current model
|
|
87
|
-
//
|
|
88
|
-
// frozen
|
|
89
|
-
const resolveRoutes = () =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
// current model with DSH `selectionFor(agent)` semantics (pending choice →
|
|
68
|
+
// logged header → deployment default). Explicit config routes are only for
|
|
69
|
+
// sessions without a DSH model source. Existing runs resume from frozen routes.
|
|
70
|
+
const resolveRoutes = () => {
|
|
71
|
+
const agent = ctx.agents.currentInitiator();
|
|
72
|
+
return resolveEffectiveRoutes({
|
|
73
|
+
configRoutes: config.routes,
|
|
74
|
+
settings: readRouteSettings(),
|
|
75
|
+
hasSession: agent !== undefined,
|
|
76
|
+
sessionSelection: sessionModelSelectionOf({
|
|
77
|
+
sessionModel: sessionModelStateOf(ctx, agent?.session),
|
|
78
|
+
requestHeader: agent?.session?.requestHeader?.(),
|
|
79
|
+
agentDefault: agent === undefined ? undefined : agentDefaultSelectionOf(ctx),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
// The DSH Session driving the current operation: a new run records it, and
|
|
84
|
+
// only that Session may answer the run's NEEDS_USER question.
|
|
85
|
+
const resolveOwnerSessionId = () => {
|
|
86
|
+
const agent = ctx.agents.currentInitiator();
|
|
87
|
+
return agent === undefined ? undefined : String(agent.session.id);
|
|
88
|
+
};
|
|
94
89
|
const serviceConfig = {
|
|
95
90
|
routes: config.routes,
|
|
96
91
|
resolveRoutes,
|
|
92
|
+
resolveOwnerSessionId,
|
|
97
93
|
browserTools: config.browserTools,
|
|
98
94
|
commanderReadOnlyTools: config.commanderReadOnlyTools,
|
|
99
95
|
watchdogTools: config.watchdogTools,
|
|
@@ -132,7 +128,7 @@ export function apply(ctx, config) {
|
|
|
132
128
|
// the parent model is never asked to decide or to run the task.
|
|
133
129
|
result = await ctx.agents.withInitiator(agent, () => service.run({
|
|
134
130
|
goal,
|
|
135
|
-
approved_loop_count:
|
|
131
|
+
approved_loop_count: HARD_ACTIVATION_LOOP_BUDGET,
|
|
136
132
|
}, cwd, signal));
|
|
137
133
|
}
|
|
138
134
|
catch (error) {
|
|
@@ -150,8 +146,9 @@ export function apply(ctx, config) {
|
|
|
150
146
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
151
147
|
installOrbitSessionProjection(projectionCtx);
|
|
152
148
|
});
|
|
153
|
-
|
|
149
|
+
{
|
|
154
150
|
const handler = createOrbitPreExecuteHandler(service, {
|
|
151
|
+
contentGuards: config.registerGuards,
|
|
155
152
|
competingDriver: (agent) => {
|
|
156
153
|
const reflect = ctx.reflect;
|
|
157
154
|
const goals = reflect?.get('goals');
|
|
@@ -193,6 +190,11 @@ function activationNotice(result) {
|
|
|
193
190
|
return undefined;
|
|
194
191
|
const lastError = result.data?.['last_error'];
|
|
195
192
|
const reason = result.message ?? (typeof lastError === 'string' && lastError !== '' ? lastError : undefined);
|
|
193
|
+
if (result.message?.startsWith('ORBIT_NEEDS_USER_OTHER_SESSION') === true || result.message?.startsWith('ORBIT_NEEDS_USER_OWNER_UNKNOWN') === true) {
|
|
194
|
+
// Another Session owns the waiting run: report instead of consuming the
|
|
195
|
+
// message as its reply.
|
|
196
|
+
return result.message;
|
|
197
|
+
}
|
|
196
198
|
if (result.phase === 'NEEDS_USER') {
|
|
197
199
|
return reason !== undefined && !reason.startsWith('COMMANDER_')
|
|
198
200
|
? `Orbit 需要你的回复:${reason}`
|
package/lib/kernel.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* read a clock, or use randomness. Runtime values (routes, timestamps, run ids)
|
|
6
6
|
* are supplied by the supervisor, which owns orchestration and persistence.
|
|
7
7
|
*/
|
|
8
|
-
import { MAX_CORRECTION_DEPTH, MAX_PLAN_STEPS, MAX_WATCHDOG_CALLS_PER_STEP, MIN_PLAN_STEPS, ORBIT_SCHEMA_VERSION, } from "./types.js";
|
|
8
|
+
import { MAX_CORRECTION_DEPTH, DEFAULT_LOOP_BUDGET, MAX_PLAN_STEPS, MAX_WATCHDOG_CALLS_PER_STEP, MIN_PLAN_STEPS, ORBIT_SCHEMA_VERSION, } from "./types.js";
|
|
9
9
|
/** Capabilities a plan step may request. */
|
|
10
|
-
export const ORBIT_CAPABILITIES = ['
|
|
10
|
+
export const ORBIT_CAPABILITIES = ['filesystem', 'shell', 'web', 'browser'];
|
|
11
11
|
const STEP_CAPABILITIES = new Set(ORBIT_CAPABILITIES);
|
|
12
12
|
const PLAN_STEP_ID = /^P\d+$/u;
|
|
13
13
|
export function normalizeCapabilities(value) {
|
|
@@ -15,6 +15,12 @@ export function normalizeCapabilities(value) {
|
|
|
15
15
|
return undefined;
|
|
16
16
|
const result = [];
|
|
17
17
|
for (const item of value) {
|
|
18
|
+
if (item === 'web-api-recon') {
|
|
19
|
+
for (const legacy of ['web', 'browser'])
|
|
20
|
+
if (!result.includes(legacy))
|
|
21
|
+
result.push(legacy);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
18
24
|
if (typeof item !== 'string' || !STEP_CAPABILITIES.has(item))
|
|
19
25
|
continue;
|
|
20
26
|
if (!result.includes(item))
|
|
@@ -22,8 +28,6 @@ export function normalizeCapabilities(value) {
|
|
|
22
28
|
}
|
|
23
29
|
if (result.length === 0)
|
|
24
30
|
return undefined;
|
|
25
|
-
if (result.includes('web-api-recon') && !result.includes('browser'))
|
|
26
|
-
result.unshift('browser');
|
|
27
31
|
return result;
|
|
28
32
|
}
|
|
29
33
|
export function normalizePlan(plan) {
|
|
@@ -46,6 +50,9 @@ export function normalizePlan(plan) {
|
|
|
46
50
|
if (steps.some((step) => !step.goal)) {
|
|
47
51
|
throw new Error('COMMANDER_PLAN_OUTPUT_INVALID: every step needs a goal');
|
|
48
52
|
}
|
|
53
|
+
if (new Set(steps.map((step) => step.id)).size !== steps.length) {
|
|
54
|
+
throw new Error('COMMANDER_PLAN_OUTPUT_INVALID: 步骤 id 不得重复');
|
|
55
|
+
}
|
|
49
56
|
return { summary: String(plan.summary ?? '').slice(0, 1000), steps };
|
|
50
57
|
}
|
|
51
58
|
export function correctionDepthOf(stepId) {
|
|
@@ -68,18 +75,6 @@ export function explicitLoopBudget(input) {
|
|
|
68
75
|
throw new Error('ORBIT_LOOP_BUDGET_INVALID: approved_loop_count above 10 requires an explicit execution request');
|
|
69
76
|
return raw;
|
|
70
77
|
}
|
|
71
|
-
export function estimateLoopCount(goal) {
|
|
72
|
-
const text = goal.toLowerCase();
|
|
73
|
-
if (/critical|migrate|migration|production|架构|重构/.test(text))
|
|
74
|
-
return 6;
|
|
75
|
-
if (/integration|联调|ui|high/.test(text))
|
|
76
|
-
return 4;
|
|
77
|
-
if (/feature|多文件|multi-file/.test(goal))
|
|
78
|
-
return 3;
|
|
79
|
-
if (/bug|fix|test/.test(text))
|
|
80
|
-
return 2;
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
83
78
|
export function updateLoopBudget(state, budget) {
|
|
84
79
|
if (budget < state.loop.used)
|
|
85
80
|
throw new Error(`ORBIT_LOOP_BUDGET_BELOW_USED: requested ${budget}, already used ${state.loop.used}`);
|
|
@@ -129,7 +124,7 @@ export function correctionBlockCode(state, step) {
|
|
|
129
124
|
}
|
|
130
125
|
export function createInitialState(input) {
|
|
131
126
|
const max = explicitLoopBudget({ approved_loop_count: input.approvedLoopCount, max_loops: input.maxLoops }) ??
|
|
132
|
-
|
|
127
|
+
DEFAULT_LOOP_BUDGET;
|
|
133
128
|
return {
|
|
134
129
|
schema_version: ORBIT_SCHEMA_VERSION,
|
|
135
130
|
active_run_id: input.runId,
|
|
@@ -142,16 +137,18 @@ export function createInitialState(input) {
|
|
|
142
137
|
goal: input.goal,
|
|
143
138
|
goal_hash: hashGoal(input.goal),
|
|
144
139
|
preset: input.preset ?? 'orbit-lite',
|
|
145
|
-
routes: input.routes,
|
|
140
|
+
routes: structuredClone(input.routes),
|
|
146
141
|
loop: { used: 0, max },
|
|
147
142
|
approved_loop_count: max,
|
|
148
143
|
remaining_budget: max,
|
|
149
144
|
loop_count: 0,
|
|
150
145
|
plan: { summary: '', steps: [] },
|
|
146
|
+
step_results: [],
|
|
151
147
|
changed_files: [],
|
|
152
148
|
test_summary: [],
|
|
153
149
|
last_error: null,
|
|
154
150
|
pending_user_reply: null,
|
|
151
|
+
...(input.ownerSessionId === undefined ? {} : { owner_session_id: input.ownerSessionId }),
|
|
155
152
|
user_hard_constraints: input.userHardConstraints ? [...input.userHardConstraints] : [],
|
|
156
153
|
github_allowed: input.githubAllowed === true,
|
|
157
154
|
interruption_retries: 0,
|
|
@@ -193,7 +190,7 @@ export function applyExecutorCapabilityUnavailable(state, stepId) {
|
|
|
193
190
|
state.last_error = 'BROWSER_CAPABILITY_UNAVAILABLE';
|
|
194
191
|
state.commander = {
|
|
195
192
|
last_decision: state.commander?.last_decision,
|
|
196
|
-
summary: `Executor
|
|
193
|
+
summary: `Executor 无法执行步骤 ${stepId}:BROWSER_CAPABILITY_UNAVAILABLE(配置的 Browser 工具不可用)。`,
|
|
197
194
|
};
|
|
198
195
|
state.phase = 'EVALUATE';
|
|
199
196
|
}
|
|
@@ -215,6 +212,17 @@ export function clearExecutorChild(state) {
|
|
|
215
212
|
state.child = undefined;
|
|
216
213
|
state.interruption_retries = 0;
|
|
217
214
|
}
|
|
215
|
+
export const MAX_STEP_RESULTS = 10;
|
|
216
|
+
/** Upsert one bounded durable result without allowing evidence to drive transitions. */
|
|
217
|
+
export function upsertStepResult(state, result) {
|
|
218
|
+
const results = state.step_results ?? [];
|
|
219
|
+
const index = results.findIndex((entry) => entry.step_id === result.step_id);
|
|
220
|
+
if (index === -1)
|
|
221
|
+
results.push(result);
|
|
222
|
+
else
|
|
223
|
+
results[index] = result;
|
|
224
|
+
state.step_results = results.slice(-MAX_STEP_RESULTS);
|
|
225
|
+
}
|
|
218
226
|
/** Apply a real Executor success: consume one loop slot, then enter EVALUATE. */
|
|
219
227
|
export function applyExecutorSuccess(state, input) {
|
|
220
228
|
state.child = { ...(input.childId ? { id: input.childId } : {}), status: 'completed' };
|
|
@@ -262,7 +270,9 @@ export function applyFinalAppend(state, decision) {
|
|
|
262
270
|
return 'budget_exhausted';
|
|
263
271
|
}
|
|
264
272
|
for (const item of appended.slice(0, remaining)) {
|
|
265
|
-
|
|
273
|
+
let index = state.plan.steps.filter((candidate) => isBaseStepId(candidate.id)).length;
|
|
274
|
+
while (state.plan.steps.some((candidate) => candidate.id === `P${index}`))
|
|
275
|
+
index += 1;
|
|
266
276
|
state.plan.steps.push({
|
|
267
277
|
id: `P${index}`,
|
|
268
278
|
goal: item.goal,
|
|
@@ -281,7 +291,9 @@ export function applyCorrectionStep(state, step, input) {
|
|
|
281
291
|
step.status = 'needs_correction';
|
|
282
292
|
const number = correctionDepthOf(step.id) + 2;
|
|
283
293
|
const insertAt = state.plan.steps.indexOf(step) + 1;
|
|
284
|
-
const correctionCapabilities =
|
|
294
|
+
const correctionCapabilities = input.capabilities === undefined
|
|
295
|
+
? step.capabilities
|
|
296
|
+
: normalizeCapabilities(input.capabilities);
|
|
285
297
|
state.plan.steps.splice(insertAt, 0, {
|
|
286
298
|
id: `${base}-${number}`,
|
|
287
299
|
goal: input.nextGoal,
|