@kasenri/dsh-orbit 0.5.7 → 0.5.9
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 +27 -19
- package/cordis.patch.yml +2 -2
- package/lib/activation.js +12 -11
- package/lib/capabilities.js +23 -0
- package/lib/client.js +47 -77
- package/lib/decisions.js +4 -4
- package/lib/dsh-host.js +112 -30
- package/lib/evidence.js +24 -0
- package/lib/guard.js +17 -17
- package/lib/index.js +64 -43
- package/lib/kernel.js +33 -21
- package/lib/pipeline-guard.js +16 -4
- package/lib/routes.js +58 -56
- 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 +135 -40
- 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";
|
|
@@ -10,6 +13,9 @@ function contentToText(blocks) {
|
|
|
10
13
|
.map((block) => (block.type === 'text' ? block.text : `[${block.type}]`))
|
|
11
14
|
.join('\n');
|
|
12
15
|
}
|
|
16
|
+
function visibleContentToText(blocks) {
|
|
17
|
+
return blocks?.flatMap((block) => block.type === 'text' && block.text.trim() !== '' ? [block.text] : []).join('\n') ?? '';
|
|
18
|
+
}
|
|
13
19
|
/**
|
|
14
20
|
* Wire the Orbit supervisor to DeepSeek Harness native Agent/Subagent services.
|
|
15
21
|
*
|
|
@@ -24,6 +30,7 @@ export class DshOrbitHost {
|
|
|
24
30
|
ownedChildren = new Set();
|
|
25
31
|
interruptedChildren = new Set();
|
|
26
32
|
childParents = new Map();
|
|
33
|
+
childGrants = new Map();
|
|
27
34
|
nowFn;
|
|
28
35
|
sleepFn;
|
|
29
36
|
constructor(ctx, options = {}) {
|
|
@@ -39,12 +46,14 @@ export class DshOrbitHost {
|
|
|
39
46
|
}
|
|
40
47
|
async startRole(request) {
|
|
41
48
|
const parent = this.parent();
|
|
49
|
+
if (request.workspace !== undefined && resolve(parent.session.header.cwd ?? process.cwd()) !== resolve(request.workspace)) {
|
|
50
|
+
throw new Error('ORBIT_WORKSPACE_MISMATCH: 子代理工作目录必须与 Orbit 持有的 workspace 一致。');
|
|
51
|
+
}
|
|
42
52
|
const prompt = [{ type: 'text', text: request.prompt }];
|
|
43
53
|
const agentOptions = {
|
|
44
54
|
provider: request.route.provider,
|
|
45
55
|
model: request.route.model,
|
|
46
|
-
|
|
47
|
-
...(request.route.maxTokens ? { maxTokens: request.route.maxTokens } : {}),
|
|
56
|
+
reasoningEffort: request.route.reasoningEffort,
|
|
48
57
|
};
|
|
49
58
|
if (request.role === 'executor') {
|
|
50
59
|
return this.startExecutor(parent, request, prompt, agentOptions);
|
|
@@ -70,11 +79,12 @@ export class DshOrbitHost {
|
|
|
70
79
|
...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
|
|
71
80
|
...(request.outputSchema ? { outputSchema: request.outputSchema } : {}),
|
|
72
81
|
}));
|
|
73
|
-
this.registerChild(run.id, parent);
|
|
82
|
+
this.registerChild(run.id, parent, request);
|
|
74
83
|
const result = run.result
|
|
75
84
|
.then((value) => ({
|
|
76
85
|
childId: run.id,
|
|
77
86
|
output: contentToText(value.output),
|
|
87
|
+
visibleOutput: visibleContentToText(value.output),
|
|
78
88
|
interrupted: value.stopReason !== 'completed',
|
|
79
89
|
...(value.stopReason !== 'completed' ? { reason: value.stopReason } : {}),
|
|
80
90
|
...(value.diagnostic ? { testSummary: [value.diagnostic] } : {}),
|
|
@@ -106,13 +116,18 @@ export class DshOrbitHost {
|
|
|
106
116
|
if (request.resumeOf) {
|
|
107
117
|
const existingId = request.resumeOf;
|
|
108
118
|
const existing = this.ctx.agents.get(existingId);
|
|
109
|
-
|
|
119
|
+
const grant = this.childGrants.get(existingId);
|
|
120
|
+
const requestedTools = new Set(request.toolFilter?.allow ?? []);
|
|
121
|
+
const sameGrant = grant?.role === 'executor' && grant.workspace === (request.workspace ?? parent.session.header.cwd ?? process.cwd())
|
|
122
|
+
&& grant.tools.size === requestedTools.size && [...grant.tools].every((tool) => requestedTools.has(tool));
|
|
123
|
+
if (existing && sameGrant && this.childParents.get(existingId) === parent && this.ctx.agents.isOwnedBy(SessionId(existingId), parent)) {
|
|
110
124
|
const agent = existing;
|
|
111
125
|
this.interruptedChildren.delete(existingId);
|
|
112
|
-
const
|
|
126
|
+
const previousTurns = (agent.session?.snapshotEvents?.() ?? []).filter((event) => event.type === 'turn/start').length;
|
|
113
127
|
await this.ctx.subagents.sendMessage(parent, existingId, prompt, {
|
|
114
|
-
signal: new AbortController().signal,
|
|
128
|
+
signal: request.signal ?? new AbortController().signal,
|
|
115
129
|
});
|
|
130
|
+
const done = this.waitForExecutorSettlement(agent, existingId, previousTurns);
|
|
116
131
|
return {
|
|
117
132
|
childId: existingId,
|
|
118
133
|
result: done,
|
|
@@ -124,19 +139,28 @@ export class DshOrbitHost {
|
|
|
124
139
|
}
|
|
125
140
|
const controller = new AbortController();
|
|
126
141
|
request.signal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
const reservedId = SessionId(randomUUID());
|
|
143
|
+
const childId = String(reservedId);
|
|
144
|
+
this.registerChild(childId, parent, request);
|
|
145
|
+
let started;
|
|
146
|
+
try {
|
|
147
|
+
started = await this.ctx.subagents.startContinuable({
|
|
148
|
+
provider: 'spawn',
|
|
149
|
+
label: request.label,
|
|
150
|
+
childId: reservedId,
|
|
151
|
+
request: {
|
|
152
|
+
prompt,
|
|
153
|
+
parent,
|
|
154
|
+
agentOptions,
|
|
155
|
+
...(request.toolFilter ? { toolFilter: request.toolFilter } : {}),
|
|
156
|
+
},
|
|
157
|
+
signal: controller.signal,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.forgetChild(childId);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
140
164
|
const agent = this.ctx.agents.get(started.childId);
|
|
141
165
|
const result = agent
|
|
142
166
|
? this.waitForExecutorSettlement(agent, childId)
|
|
@@ -149,19 +173,26 @@ export class DshOrbitHost {
|
|
|
149
173
|
runtimeSnapshot: agent ? () => this.snapshotAgent(agent) : undefined,
|
|
150
174
|
};
|
|
151
175
|
}
|
|
152
|
-
registerChild(childId, parent) {
|
|
176
|
+
registerChild(childId, parent, request) {
|
|
153
177
|
this.ownedChildren.add(childId);
|
|
154
178
|
this.childParents.set(childId, parent);
|
|
179
|
+
this.childGrants.set(childId, {
|
|
180
|
+
role: request.role,
|
|
181
|
+
workspace: resolve(request.workspace ?? parent.session.header.cwd ?? process.cwd()),
|
|
182
|
+
tools: new Set(request.toolFilter?.allow ?? []),
|
|
183
|
+
});
|
|
155
184
|
}
|
|
156
185
|
forgetChild(childId) {
|
|
157
186
|
this.ownedChildren.delete(childId);
|
|
158
187
|
this.interruptedChildren.delete(childId);
|
|
159
188
|
this.childParents.delete(childId);
|
|
189
|
+
this.childGrants.delete(childId);
|
|
160
190
|
}
|
|
161
191
|
async interruptExecutor(childId, reason) {
|
|
162
192
|
this.interruptedChildren.add(childId);
|
|
163
193
|
const parent = this.childParents.get(childId);
|
|
164
194
|
this.ctx.subagents.interrupt(childId, parent ? { kind: 'ancestor', agent: parent } : { kind: 'user', parentSessionId: childId });
|
|
195
|
+
await this.ctx.agents.get(SessionId(childId))?.whenIdle();
|
|
165
196
|
void reason;
|
|
166
197
|
}
|
|
167
198
|
async drainExecutor(parent, childId) {
|
|
@@ -173,8 +204,8 @@ export class DshOrbitHost {
|
|
|
173
204
|
}
|
|
174
205
|
this.forgetChild(childId);
|
|
175
206
|
}
|
|
176
|
-
async waitForExecutorSettlement(agent, childId) {
|
|
177
|
-
await this.waitForTurnOrIdle(agent);
|
|
207
|
+
async waitForExecutorSettlement(agent, childId, previousTurns = 0) {
|
|
208
|
+
await this.waitForTurnOrIdle(agent, previousTurns);
|
|
178
209
|
const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
|
|
179
210
|
const classified = classifyTurnSettlement(events);
|
|
180
211
|
const output = this.readFinalOutput(agent);
|
|
@@ -214,20 +245,22 @@ export class DshOrbitHost {
|
|
|
214
245
|
return { childId, output, interrupted: true, reason: 'EXECUTOR_BLOCKED', telemetry, ...evidence };
|
|
215
246
|
case 'max-tokens':
|
|
216
247
|
return { childId, output, interrupted: true, reason: 'EXECUTOR_MAX_TOKENS', telemetry, ...evidence };
|
|
248
|
+
case 'interrupted':
|
|
249
|
+
return { childId, output, interrupted: true, reason: 'EXECUTOR_INTERRUPTED', telemetry, ...evidence };
|
|
217
250
|
default:
|
|
218
251
|
return { childId, output, interrupted: true, reason: 'EXECUTOR_NO_TURN', telemetry, ...evidence };
|
|
219
252
|
}
|
|
220
253
|
}
|
|
221
|
-
async waitForTurnOrIdle(agent) {
|
|
254
|
+
async waitForTurnOrIdle(agent, previousTurns = 0) {
|
|
222
255
|
const deadline = this.nowFn() + EXECUTOR_TURN_START_TIMEOUT_MS;
|
|
223
|
-
while (this.nowFn() < deadline && !this.hasTurnStarted(agent)) {
|
|
256
|
+
while (this.nowFn() < deadline && !this.hasTurnStarted(agent, previousTurns)) {
|
|
224
257
|
await this.sleepFn(20);
|
|
225
258
|
}
|
|
226
259
|
await agent.whenIdle?.();
|
|
227
260
|
}
|
|
228
|
-
hasTurnStarted(agent) {
|
|
261
|
+
hasTurnStarted(agent, previousTurns) {
|
|
229
262
|
const events = agent.session?.snapshotEvents?.() ?? [];
|
|
230
|
-
return events.
|
|
263
|
+
return events.filter((event) => event.type === 'turn/start').length > previousTurns;
|
|
231
264
|
}
|
|
232
265
|
readFinalOutput(agent) {
|
|
233
266
|
const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
|
|
@@ -308,6 +341,53 @@ export class DshOrbitHost {
|
|
|
308
341
|
const agent = this.ctx.agents.currentInitiator();
|
|
309
342
|
return this.ctx.tools.get(name, agent) !== undefined;
|
|
310
343
|
}
|
|
344
|
+
async validateRoutes(routes, signal) {
|
|
345
|
+
const issues = [];
|
|
346
|
+
const labels = { commander: '指挥官', executor: '执行员', watchdog: '监控模型' };
|
|
347
|
+
for (const role of ['commander', 'executor', 'watchdog']) {
|
|
348
|
+
const route = routes[role];
|
|
349
|
+
try {
|
|
350
|
+
const llm = this.ctx.reflect.get('llm');
|
|
351
|
+
if (!llm)
|
|
352
|
+
throw new Error('DSH LLM registry 不可用');
|
|
353
|
+
const deadline = AbortSignal.timeout(10_000);
|
|
354
|
+
const activeSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
355
|
+
const info = await llm.resolveModelInfo(route.provider, route.model, activeSignal);
|
|
356
|
+
const catalog = await llm.listModels(route.provider);
|
|
357
|
+
if (catalog.length > 0 && !catalog.some((entry) => entry.id === route.model)) {
|
|
358
|
+
issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前模型目录中不存在,请重新选择。`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (route.reasoningEffort !== undefined) {
|
|
362
|
+
const efforts = info.reasoning?.efforts ?? [];
|
|
363
|
+
if (!efforts.some((effort) => effort.id === route.reasoningEffort)) {
|
|
364
|
+
issues.push(`${labels[role]}:ORBIT_REASONING_EFFORT_UNAVAILABLE (${route.provider}/${route.model}/${route.reasoningEffort}),当前模型未声明此推理等级。`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前不可用,请重新选择:${truncateSafe(error instanceof Error ? error.message : String(error), 200)}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return issues;
|
|
373
|
+
}
|
|
374
|
+
isMutationAuthorized(agent, cwd, tool) {
|
|
375
|
+
const id = String(agent?.id ?? '');
|
|
376
|
+
const grant = this.childGrants.get(id);
|
|
377
|
+
const parent = this.childParents.get(id);
|
|
378
|
+
return grant?.role === 'executor' && grant.workspace === resolve(cwd) && grant.tools.has(tool)
|
|
379
|
+
&& !this.interruptedChildren.has(id) && this.ctx.agents.get(SessionId(id)) === agent
|
|
380
|
+
&& parent !== undefined && this.ctx.agents.isOwnedBy(SessionId(id), parent);
|
|
381
|
+
}
|
|
382
|
+
async revokeWorkspace(cwd) {
|
|
383
|
+
const ids = [...this.childGrants].filter(([, grant]) => grant.workspace === resolve(cwd) && grant.role === 'executor').map(([id]) => id);
|
|
384
|
+
for (const id of ids) {
|
|
385
|
+
const parent = this.childParents.get(id);
|
|
386
|
+
await this.interruptExecutor(id, 'ORBIT_WORKSPACE_RELEASED');
|
|
387
|
+
if (parent)
|
|
388
|
+
await this.drainExecutor(parent, id);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
311
391
|
/**
|
|
312
392
|
* Mutation ownership is about top-level autonomous drivers, not about every
|
|
313
393
|
* running agent. The calling parent, Orbit's own children, and ordinary
|
|
@@ -316,17 +396,19 @@ export class DshOrbitHost {
|
|
|
316
396
|
* at tool start by the Orbit mutation guard.
|
|
317
397
|
*/
|
|
318
398
|
async otherMutationDrivers(cwd) {
|
|
319
|
-
void cwd;
|
|
320
399
|
const drivers = [];
|
|
321
400
|
const initiator = this.ctx.agents.currentInitiator();
|
|
322
401
|
// `ctx.reflect.get` is the official service lookup that does not require an
|
|
323
402
|
// inject declaration, so Orbit stays loadable in profiles without dsh-goal.
|
|
324
403
|
const reflect = this.ctx.reflect;
|
|
325
404
|
const goals = reflect?.get('goals');
|
|
326
|
-
|
|
405
|
+
const agents = this.ctx.agents.list?.() ?? (initiator ? [initiator] : []);
|
|
406
|
+
for (const candidate of agents) {
|
|
407
|
+
if (!goals || resolve(candidate.session.header.cwd ?? process.cwd()) !== resolve(cwd) || this.ownedChildren.has(String(candidate.id)))
|
|
408
|
+
continue;
|
|
327
409
|
try {
|
|
328
|
-
const goal = goals.get(
|
|
329
|
-
if (goal?.phase === 'active')
|
|
410
|
+
const goal = goals.get(candidate);
|
|
411
|
+
if (goal?.phase === 'active' && !drivers.includes('goal'))
|
|
330
412
|
drivers.push('goal');
|
|
331
413
|
}
|
|
332
414
|
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
|
-
import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
1
|
+
import { AssistantStreamAccumulator, boundContextSummary, createAssistantMessage, 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,66 +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
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// `state.routes`.
|
|
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.
|
|
90
70
|
const resolveRoutes = () => {
|
|
91
71
|
const agent = ctx.agents.currentInitiator();
|
|
92
72
|
return resolveEffectiveRoutes({
|
|
93
73
|
configRoutes: config.routes,
|
|
94
|
-
settings:
|
|
95
|
-
|
|
96
|
-
sessionSelection:
|
|
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
|
+
}),
|
|
97
81
|
});
|
|
98
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
|
+
};
|
|
99
89
|
const serviceConfig = {
|
|
100
90
|
routes: config.routes,
|
|
101
91
|
resolveRoutes,
|
|
92
|
+
resolveOwnerSessionId,
|
|
102
93
|
browserTools: config.browserTools,
|
|
103
94
|
commanderReadOnlyTools: config.commanderReadOnlyTools,
|
|
104
95
|
watchdogTools: config.watchdogTools,
|
|
@@ -129,7 +120,7 @@ export function apply(ctx, config) {
|
|
|
129
120
|
});
|
|
130
121
|
installOrbitGestureBoundary(ctx, {
|
|
131
122
|
sessionEnabled: (session) => orbitEnabledOf(ctx, session),
|
|
132
|
-
activate: async (agent, goal, signal) => {
|
|
123
|
+
activate: async (agent, goal, position, signal) => {
|
|
133
124
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
134
125
|
let result;
|
|
135
126
|
try {
|
|
@@ -137,13 +128,17 @@ export function apply(ctx, config) {
|
|
|
137
128
|
// the parent model is never asked to decide or to run the task.
|
|
138
129
|
result = await ctx.agents.withInitiator(agent, () => service.run({
|
|
139
130
|
goal,
|
|
140
|
-
approved_loop_count:
|
|
131
|
+
approved_loop_count: HARD_ACTIVATION_LOOP_BUDGET,
|
|
141
132
|
}, cwd, signal));
|
|
142
133
|
}
|
|
143
134
|
catch (error) {
|
|
144
135
|
appendOrbitNotice(agent.session, `Orbit 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
145
136
|
return;
|
|
146
137
|
}
|
|
138
|
+
if (result.ok && result.phase === 'SUCCESS' && result.final_output !== undefined) {
|
|
139
|
+
appendOrbitFinalOutput(agent.session, position, result.final_output);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
147
142
|
const notice = activationNotice(result);
|
|
148
143
|
if (notice !== undefined)
|
|
149
144
|
appendOrbitNotice(agent.session, notice);
|
|
@@ -155,8 +150,9 @@ export function apply(ctx, config) {
|
|
|
155
150
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
156
151
|
installOrbitSessionProjection(projectionCtx);
|
|
157
152
|
});
|
|
158
|
-
|
|
153
|
+
{
|
|
159
154
|
const handler = createOrbitPreExecuteHandler(service, {
|
|
155
|
+
contentGuards: config.registerGuards,
|
|
160
156
|
competingDriver: (agent) => {
|
|
161
157
|
const reflect = ctx.reflect;
|
|
162
158
|
const goals = reflect?.get('goals');
|
|
@@ -176,6 +172,26 @@ export function apply(ctx, config) {
|
|
|
176
172
|
ctx.on('tools/pre-execute', (exec, next) => handler(exec, next));
|
|
177
173
|
}
|
|
178
174
|
}
|
|
175
|
+
/** Append the existing Final Commander answer as this consumed turn's sole assistant result. */
|
|
176
|
+
function appendOrbitFinalOutput(session, position, output) {
|
|
177
|
+
const message = createAssistantMessage({
|
|
178
|
+
content: [{ type: 'text', text: output.text }],
|
|
179
|
+
source: { provider: output.provider, model: output.model },
|
|
180
|
+
});
|
|
181
|
+
const stream = new AssistantStreamAccumulator();
|
|
182
|
+
const time = Date.now();
|
|
183
|
+
stream.push({ time, chunk: { type: 'block-start', index: 0, blockType: 'text' } });
|
|
184
|
+
stream.push({ time, chunk: { type: 'text-delta', index: 0, text: output.text } });
|
|
185
|
+
stream.push({ time, chunk: { type: 'block-end', index: 0, block: { type: 'text', text: output.text } } });
|
|
186
|
+
stream.push({ time, chunk: { type: 'finish', reason: { kind: 'stop' } } });
|
|
187
|
+
session.append('step/start', position);
|
|
188
|
+
try {
|
|
189
|
+
session.append('assistant/message', { ...position, message, stream: [...stream.snapshot()] }, { surfaceOp: 'append' });
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
session.append('step/end', position);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
179
195
|
/**
|
|
180
196
|
* One durable, user-visible Orbit notice. The DSH `plugin` + `form: 'notice'`
|
|
181
197
|
* source renders as a collapsed notice row instead of a user message, and the
|
|
@@ -198,6 +214,11 @@ function activationNotice(result) {
|
|
|
198
214
|
return undefined;
|
|
199
215
|
const lastError = result.data?.['last_error'];
|
|
200
216
|
const reason = result.message ?? (typeof lastError === 'string' && lastError !== '' ? lastError : undefined);
|
|
217
|
+
if (result.message?.startsWith('ORBIT_NEEDS_USER_OTHER_SESSION') === true || result.message?.startsWith('ORBIT_NEEDS_USER_OWNER_UNKNOWN') === true) {
|
|
218
|
+
// Another Session owns the waiting run: report instead of consuming the
|
|
219
|
+
// message as its reply.
|
|
220
|
+
return result.message;
|
|
221
|
+
}
|
|
201
222
|
if (result.phase === 'NEEDS_USER') {
|
|
202
223
|
return reason !== undefined && !reason.startsWith('COMMANDER_')
|
|
203
224
|
? `Orbit 需要你的回复:${reason}`
|