@dsh-workbench/shared 0.1.10 → 0.1.11

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.
@@ -104,6 +104,7 @@ export interface DshWorkspaceWritten {
104
104
  readonly title?: string;
105
105
  }
106
106
  export interface DshWorkspaces {
107
+ readonly list: DshObservable<WorkspaceSnapshot>;
107
108
  create(request: {
108
109
  path: string;
109
110
  }): Promise<DshWorkspaceWritten | undefined>;
@@ -159,6 +160,12 @@ export interface DshSessionsService {
159
160
  getSnapshot(): SessionsSnapshot;
160
161
  };
161
162
  scope(sessionId: string): unknown | undefined;
163
+ create?(request: {
164
+ readonly workspaceId?: string;
165
+ readonly sessionId?: string;
166
+ readonly cwd?: string;
167
+ readonly agentPreset?: string;
168
+ }): Promise<unknown>;
162
169
  }
163
170
  export interface DshObservable<T> {
164
171
  getSnapshot(): T;
@@ -171,6 +178,18 @@ export interface DshSidebarRight {
171
178
  openResource(address: string, options?: unknown): void;
172
179
  openTab(kind: string, options?: unknown): void;
173
180
  }
181
+ export interface DshSessionsRemote {
182
+ create(request: {
183
+ readonly workspaceId?: string;
184
+ readonly cwd?: string;
185
+ readonly agentPreset?: string;
186
+ }): Promise<{
187
+ readonly sessionId: string;
188
+ }>;
189
+ }
190
+ export interface DshClientRemote {
191
+ readonly sessions: DshSessionsRemote;
192
+ }
174
193
  export interface DshClientContext {
175
194
  get<T = unknown>(name: string): T | undefined;
176
195
  provide(name: string, value: unknown): void;
@@ -181,4 +200,5 @@ export interface DshClientContext {
181
200
  readonly uiWorkspace: DshUiWorkspace;
182
201
  readonly workspaces: DshWorkspaces;
183
202
  readonly shortcuts: DshShortcutCatalog;
203
+ readonly remote: DshClientRemote;
184
204
  }
package/dist/api/dsh.d.ts CHANGED
@@ -91,6 +91,7 @@ export interface DshHostContext {
91
91
  get<T = unknown>(name: string): T | undefined;
92
92
  provide(name: string, value: unknown): () => void;
93
93
  effect(effect: () => void | (() => void), label?: string): () => void;
94
+ inject(names: readonly string[], effect: (scoped: DshHostContext) => void): () => void;
94
95
  readonly logger?: DshLogger;
95
96
  readonly webServer: {
96
97
  register(route: DshWebRoute): () => void;
@@ -128,7 +129,16 @@ export interface DshWorkspaceRegistry {
128
129
  path?: string;
129
130
  title?: string;
130
131
  } | undefined;
132
+ resolveByPath(path: string): Promise<{
133
+ readonly id: unknown;
134
+ } | undefined>;
135
+ create(path: string, title?: string): Promise<{
136
+ id?: string;
137
+ path?: string;
138
+ title?: string;
139
+ } | undefined>;
131
140
  }
141
+ export type DshHomePath = (relative: string) => string;
132
142
  export interface DshToolDefinition {
133
143
  name: string;
134
144
  description: string;
@@ -21,9 +21,18 @@ export interface ApprovalCall {
21
21
  readonly arguments?: unknown;
22
22
  readonly workspace?: string | undefined;
23
23
  }
24
+ export type ApprovalVerdict = {
25
+ readonly kind: 'read';
26
+ } | {
27
+ readonly kind: 'ask';
28
+ readonly reason: string;
29
+ readonly question: ApprovalDetail;
30
+ readonly fallback: ApprovalDetail;
31
+ };
32
+ export type ApprovalPolicy = (call: ApprovalCall) => ApprovalVerdict | undefined;
24
33
  export interface ApprovalContext {
25
- readonly titleOf?: ((id: string) => string | undefined) | undefined;
26
34
  readonly writes?: boolean | undefined;
35
+ readonly policy?: ApprovalPolicy | undefined;
27
36
  readonly detailOf?: ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>) | undefined;
28
37
  }
29
38
  export declare function approvalFor(call: ApprovalCall, context?: ApprovalContext): Promise<AskDecision | undefined>;
@@ -1,190 +1,45 @@
1
- function compose(detail, texts) {
2
- if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '')
3
- return texts.generic;
4
- return { en: `${detail.en} — ${texts.question.en}`, zh: `${detail.zh} —— ${texts.question.zh}` };
1
+ function messageOf(error) {
2
+ return error instanceof Error ? error.message : String(error);
5
3
  }
6
- function actionOf(call) {
7
- const raw = call.arguments;
8
- if (typeof raw !== 'object' || raw === null)
9
- return {};
10
- const value = raw;
4
+ function compose(detail, verdict) {
5
+ if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '') {
6
+ return verdict.fallback;
7
+ }
11
8
  return {
12
- ...(typeof value.action === 'string' ? { action: value.action } : {}),
13
- ...(typeof value.id === 'string' ? { id: value.id } : typeof value.unit === 'string' ? { id: value.unit } : {}),
9
+ en: `${detail.en} — ${verdict.question.en}`,
10
+ zh: `${detail.zh} —— ${verdict.question.zh}`,
14
11
  };
15
12
  }
16
- const READ_ONLY_ACTIONS = new Set(['list', '']);
17
- const READ = { kind: 'read' };
18
- const UNKNOWN = { kind: 'unknown' };
19
- function ask(spec) {
20
- return { kind: 'ask', spec };
21
- }
22
- function confirmed(call) {
23
- const raw = call.arguments;
24
- if (typeof raw !== 'object' || raw === null)
13
+ export async function approvalFor(call, context = {}) {
14
+ if (context.writes !== true)
25
15
  return undefined;
26
- const value = raw.confirm;
27
- return typeof value === 'boolean' ? value : undefined;
28
- }
29
- function effectOf(call, context) {
30
- const name = call.name ?? '';
31
- const { action, id } = actionOf(call);
32
- const readAction = action !== undefined && action !== '' && READ_ONLY_ACTIONS.has(action);
33
- const what = () => {
34
- if (id === undefined || id === '')
35
- return '一个模块';
36
- return `「${context.titleOf?.(id) ?? id}」`;
37
- };
38
- if (name === 'workbench_cmdb_release') {
39
- if (confirmed(call) !== true)
40
- return READ;
41
- return ask({
42
- reason: 'confirm=true releases a version to a real environment',
43
- generic: {
44
- en: 'The agent wants to deploy a version for real (confirm: true) — it deploys on the platform (non-production). Allow?',
45
- zh: 'agent 想在非生产环境上**真的部署一个版本**(confirm: true)—— 会在平台上部署。允许吗?',
46
- },
47
- question: {
48
- en: 'it deploys on the platform for real (non-production). Allow?',
49
- zh: '会在平台上真的部署一次(非生产环境)。允许吗?',
50
- },
51
- });
52
- }
53
- if (name === 'workbench_coding_issue_comment' || name === 'workbench_coding_issue_transfer') {
54
- if (confirmed(call) !== true)
55
- return READ;
56
- const comment = name === 'workbench_coding_issue_comment';
57
- return ask({
58
- reason: comment
59
- ? 'confirm=true posts a comment on the platform'
60
- : 'confirm=true moves an issue to another status on the platform',
61
- generic: comment
62
- ? {
63
- en: 'The agent wants to post a comment on the platform for real (confirm: true) — it writes on the platform. Allow?',
64
- zh: 'agent 想**真的给一条缺陷加评论**(confirm: true)—— 会写平台数据。允许吗?',
65
- }
66
- : {
67
- en: 'The agent wants to move an issue to another status for real (confirm: true) — it writes on the platform. Allow?',
68
- zh: 'agent 想**真的把一条缺陷流转到别的状态**(confirm: true)—— 会写平台数据。允许吗?',
69
- },
70
- question: comment
71
- ? {
72
- en: 'it posts that comment on the platform for real. Allow?',
73
- zh: '会在平台上真的发表这条评论。允许吗?',
74
- }
75
- : {
76
- en: 'it changes that issue status on the platform for real. Allow?',
77
- zh: '会在平台上真的改这条缺陷的状态。允许吗?',
78
- },
79
- });
80
- }
81
- if (name === 'workbench_modules') {
82
- if (readAction)
83
- return READ;
84
- if (action === 'update-dependency') {
85
- return ask({
86
- reason: `workbench_modules update-dependency ${id ?? ''} upgrades a shared dependency on this machine`,
87
- generic: {
88
- en: `The agent wants to upgrade the shared dependency ${what()} (it changes what is installed on this machine). Allow?`,
89
- zh: `agent 想升级共用依赖 ${what()}(会改这台机器上装的东西)。允许吗?`,
90
- },
91
- question: {
92
- en: 'it upgrades that shared dependency on this machine. Allow?',
93
- zh: `会升级共用依赖 ${what()}。允许吗?`,
94
- },
95
- });
96
- }
97
- const verb = action === 'install' ? '装' : action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '装 / 更新 / 卸载';
98
- return ask({
99
- reason: `workbench_modules ${action ?? '(unknown action)'} ${id ?? ''} changes plugins on this machine`.trim(),
100
- generic: {
101
- en: `The agent wants to ${verb === '装' ? 'install' : verb === '卸载' ? 'remove' : verb === '更新' ? 'update' : 'install/update/remove'} the module ${what()} (it changes plugins on this machine). Allow?`,
102
- zh: `agent 想${verb}模块 ${what()}(会改这台机器上的插件)。允许吗?`,
103
- },
104
- question: {
105
- en: 'it changes plugins on this machine. Allow?',
106
- zh: '会改这台机器上的插件。允许吗?',
107
- },
108
- });
109
- }
110
- if (name !== 'workbench_panels')
111
- return UNKNOWN;
112
- if (readAction)
113
- return READ;
114
- if (action === 'hide') {
115
- return ask({
116
- reason: 'workbench_panels hide removes a module from the panel',
117
- generic: {
118
- en: `The agent wants to remove ${what()} from this project's panel (its agent tools go offline here too). Allow?`,
119
- zh: `agent 想把 ${what()} 从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
120
- },
121
- question: {
122
- en: 'it takes that module off this panel (its agent tools go offline here too). Allow?',
123
- zh: `${what()}会从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
124
- },
125
- });
126
- }
127
- if (action === 'move') {
128
- return ask({
129
- reason: 'workbench_panels move reorders the panel',
130
- generic: {
131
- en: `The agent wants to reorder this project's panel (put ${what()} first). Allow?`,
132
- zh: `agent 想调整这个项目面板的顺序(把 ${what()} 放前面)。允许吗?`,
133
- },
134
- question: {
135
- en: `it reorders this panel (${what()} goes first). Allow?`,
136
- zh: `会调整这个项目面板的顺序(${what()}放前面)。允许吗?`,
137
- },
138
- });
16
+ let verdict;
17
+ try {
18
+ verdict = context.policy?.(call);
139
19
  }
140
- if (action === 'reset') {
141
- return ask({
142
- reason: 'workbench_panels reset clears the panel',
143
- generic: {
144
- en: "The agent wants to clear this project's panel (every module goes offline here). Allow?",
145
- zh: 'agent 想清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
146
- },
147
- question: {
148
- en: 'it clears this panel (every module goes offline here). Allow?',
149
- zh: '会清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
150
- },
151
- });
20
+ catch (error) {
21
+ return {
22
+ kind: 'deny',
23
+ reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf(error)}`,
24
+ };
152
25
  }
153
- return ask({
154
- reason: `workbench_panels ${action ?? '(unknown action)'} changes the user's panel`,
155
- generic: {
156
- en: `The agent wants to add ${what()} to this project's panel. Allow?`,
157
- zh: `agent 想把 ${what()} 加到**这个项目**的面板上。允许吗?`,
158
- },
159
- question: {
160
- en: `it adds ${what()} to this project's panel. Allow?`,
161
- zh: `会把 ${what()} 加到**这个项目**的面板上。允许吗?`,
162
- },
163
- });
164
- }
165
- export async function approvalFor(call, context = {}) {
166
- const effect = effectOf(call, context);
167
- if (effect.kind === 'read')
26
+ if (verdict?.kind === 'read')
168
27
  return undefined;
169
- if (effect.kind === 'unknown') {
170
- if (context.writes !== true)
171
- return undefined;
28
+ if (verdict?.kind !== 'ask') {
172
29
  return {
173
30
  kind: 'deny',
174
- reason: `${call.name ?? '这把工具'} 在清单里声明会写东西,但审批表没认领这次调用 —— 停下:` +
175
- '给它在 tool-approval.ts 里补一条审批(含"这次会发生什么"),别让写静默过去',
31
+ reason: `${call.name ?? '这把工具'} 在清单里声明会写东西,但它的套件没认领这次调用 —— 停下:` +
32
+ '在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去',
176
33
  };
177
34
  }
178
- const { spec } = effect;
179
35
  let facts;
180
36
  try {
181
37
  facts = await context.detailOf?.(call);
182
38
  }
183
39
  catch (error) {
184
- const why = error instanceof Error ? error.message : String(error);
185
- return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${why}` };
40
+ return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${messageOf(error)}` };
186
41
  }
187
42
  if (facts !== undefined && 'blocked' in facts)
188
43
  return { kind: 'deny', reason: facts.blocked };
189
- return { kind: 'ask', reason: spec.reason, displayReason: compose(facts, spec) };
44
+ return { kind: 'ask', reason: verdict.reason, displayReason: compose(facts, verdict) };
190
45
  }
@@ -4,7 +4,7 @@ import type { RouteTable } from '../host/http.js';
4
4
  import type { LongTaskBase, LongTaskHost, LongTaskRegistry } from '../host/long-task.js';
5
5
  import type { ProvisionHandler } from '../host/provision.js';
6
6
  import type { UnitCapability } from './unit-libs.js';
7
- import type { ApprovalCall, ApprovalFacts } from '../host/tool-approval.js';
7
+ import type { ApprovalCall, ApprovalFacts, ApprovalPolicy } from '../host/tool-approval.js';
8
8
  import type { ValueHandoff } from '../host/value-handoff.js';
9
9
  import type { WorkspaceStore } from '../host/workspace-store.js';
10
10
  export interface UnitMountDeps {
@@ -59,6 +59,7 @@ export interface UnitMountResult {
59
59
  readonly provision?: ProvisionHandler;
60
60
  readonly longTasks?: LongTaskRegistry<LongTaskBase> | undefined;
61
61
  readonly approvalDetail?: ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>) | undefined;
62
+ readonly needsApproval?: ApprovalPolicy | undefined;
62
63
  }
63
64
  export interface UnitHostFace {
64
65
  readonly manifest: UnitManifest;
package/dist/i18n/en.js CHANGED
@@ -44,6 +44,7 @@ export const en = {
44
44
  'panels.enabled': 'enabled in this project',
45
45
  'panels.disabled': 'not enabled in this project',
46
46
  'panels.choose': 'Choose modules',
47
+ 'panels.buildModule': 'Build a module',
47
48
  'panels.failed': 'Could not save the change. Try again.',
48
49
  'panels.hintEffect': 'Disabling = the agent cannot reach that data',
49
50
  'panels.dragHint': 'Drag to reorder',
@@ -46,6 +46,7 @@ export declare const dictionaries: {
46
46
  readonly 'panels.enabled': "这个项目已启用";
47
47
  readonly 'panels.disabled': "这个项目未启用";
48
48
  readonly 'panels.choose': "选择模块";
49
+ readonly 'panels.buildModule': "造一个模块";
49
50
  readonly 'panels.failed': "改配置失败了,再试一次。";
50
51
  readonly 'panels.hintEffect': "停用 = agent 拿不到相关数据";
51
52
  readonly 'panels.dragHint': "拖动调整顺序";
@@ -309,7 +310,7 @@ export declare const dictionaries: {
309
310
  readonly 'panel.conversation': "对话";
310
311
  readonly 'panels.label': "全局面板";
311
312
  };
312
- en: Record<"time.now" | "time.minutes" | "time.hours" | "time.days" | "blocker.retry" | "blocker.copy" | "blocker.unknown.title" | "blocker.login.go" | "blocker.login.titleGeneric" | "blocker.login.title" | "blocker.login.manual" | "blocker.login.hint" | "blocker.forbidden.open" | "blocker.forbidden.titleGeneric" | "blocker.forbidden.title" | "blocker.forbidden.hint" | "blocker.timeout.hint" | "blocker.exec.hint" | "blocker.fix.install" | "blocker.fix.update" | "blocker.market.open" | "env.unit.failed" | "env.unit.failedHint" | "env.unit.missingPlugin" | "env.unit.hint" | "env.unit.missingCommands" | "env.unit.missingCommandsHint" | "env.unit.unchecked" | "env.unit.uncheckedHint" | "brand.name" | "toggle.open" | "toggle.collapse" | "project.label" | "workspace.section" | "project.none" | "project.empty" | "project.add" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.rename.row" | "project.rename.current" | "project.rename.title" | "project.rename.description" | "project.rename.field" | "project.rename.placeholder" | "project.rename.hint" | "project.rename.save" | "project.rename.saving" | "project.rename.cancel" | "project.rename.failed" | "session.new" | "session.new.label" | "session.empty" | "session.noProject" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "panels.title" | "panels.reset" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.failed" | "panels.hintEffect" | "panels.dragHint" | "panels.unknownStep" | "panels.labelWithHint" | "panels.dep.failed" | "panels.dep.missingPlugin" | "panels.dep.missingCommands" | "panels.dep.unchecked" | "panels.tab.modules" | "panels.tab.market" | "panels.browseMarket" | "panels.noneInstalled" | "panels.noneInstalledHint" | "panels.market.installed" | "panels.market.outdated" | "panels.market.hasNew" | "panels.market.missing" | "panels.market.unchecked" | "panels.market.checking" | "panels.market.sectionInstalled" | "panels.market.sectionPending" | "panels.market.sectionShared" | "panels.market.sharedUsedBy" | "panels.market.listSeparator" | "panels.market.sharedMissing" | "panels.market.sharedOutdated" | "panels.market.sharedUnsatisfied" | "panels.market.sharedOk" | "panels.market.versionArrow" | "panels.market.lead" | "panels.market.install" | "panels.market.update" | "panels.market.uninstall" | "panels.market.uninstallLabel" | "panels.market.installing" | "panels.market.updating" | "panels.market.uninstalling" | "panels.market.confirmUninstallTitle" | "panels.market.confirmUninstallDetail" | "panels.market.confirmUninstallAck" | "panels.market.confirmYes" | "panels.market.confirmNo" | "panels.market.failed" | "panels.market.needEnv" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.installedSection" | "panels.market.moduleOutdated" | "panels.market.moduleRestartHint" | "panels.market.restartNeeded" | "panels.market.foot" | "panels.market.recheck" | "panels.market.checkUpdates" | "panels.market.checkedAt" | "panels.market.justNow" | "panels.market.minutesAgo" | "panels.market.hoursAgo" | "panels.market.add.description" | "panels.market.add.specPlaceholder" | "panels.market.add.guideToggle" | "panels.market.add.guideHide" | "panels.market.add.guideIdTitle" | "panels.market.add.guideIdExample" | "panels.market.add.guideIdHint" | "panels.market.add.guideGitTitle" | "panels.market.add.guideGitExample" | "panels.market.add.guideGitHint" | "panels.market.add.guidePathTitle" | "panels.market.add.guidePathExample" | "panels.market.add.guidePathHint" | "panels.market.add.guideExampleLabel" | "panels.market.add.guideFill" | "panels.market.add.guideSafety" | "panels.market.add.registryLegend" | "panels.market.add.registryOfficial" | "panels.market.add.registryMirror" | "panels.market.add.registryWithHost" | "panels.market.add.registryToggle" | "panels.market.add.installVersion" | "panels.market.add.registryDefault" | "panels.market.add.registryCustom" | "panels.market.add.registryCustomPlaceholder" | "panels.market.add.registryCustomHint" | "panels.market.add.registryCustomInvalid" | "panels.market.add.run" | "panels.market.add.edit" | "panels.market.add.changeRegistry" | "panels.market.add.installingTitle" | "panels.market.add.installedTitle" | "panels.market.add.failedTitle" | "panels.market.add.cancelledTitle" | "panels.market.add.failureNetworkHost" | "panels.market.add.problemInvalid" | "panels.market.add.problemInstalled" | "panels.market.add.problemNotFound" | "panels.market.add.problemNotPackage" | "panels.market.add.problemNotBundle" | "panels.market.add.problemNetwork" | "panels.market.add.problemUnknown" | "panels.market.add.failureNetwork" | "panels.market.add.failureNotFound" | "panels.market.add.failureNoMatchingVersion" | "panels.market.add.failureDiskFull" | "panels.market.add.failurePermission" | "panels.market.add.failureBuildBlocked" | "panels.market.add.failureIntegrity" | "panels.market.add.failureTimeout" | "panels.market.add.failurePnpmMissing" | "panels.market.add.failureGeneric" | "panels.market.add.approveTitle" | "panels.market.add.approveCaution" | "panels.market.add.approveRun" | "panels.market.add.restart" | "panels.market.add.tookEffect" | "panels.market.add.details" | "panels.market.add.open" | "panels.market.add.title" | "panels.market.add.intro" | "panels.market.add.specLabel" | "panels.market.add.specHint" | "panels.market.add.exampleTitle" | "panels.market.add.exampleFill" | "panels.market.add.registryLabel" | "panels.market.add.registryProfile" | "panels.market.add.registryPnpm" | "panels.market.add.check" | "panels.market.add.checking" | "panels.market.add.change" | "panels.market.add.previewTitle" | "panels.market.add.fieldName" | "panels.market.add.fieldVersion" | "panels.market.add.fieldDescription" | "panels.market.add.fieldSource" | "panels.market.add.install" | "panels.market.add.installing" | "panels.market.add.cancel" | "panels.market.add.close" | "panels.market.add.retry" | "panels.market.add.changeSource" | "panels.market.add.approveBuilds" | "panels.market.add.approveHint" | "panels.market.add.doneApplied" | "panels.market.add.doneRestart" | "panels.market.add.cancelled" | "panels.market.add.failed" | "panels.market.add.removed" | "panels.market.add.supplyPending" | "panels.market.add.supplyFailed" | "panels.market.add.notModule" | "panels.market.add.removedRestart" | "panels.market.add.remove" | "panels.market.add.problem.invalidSpec" | "panels.market.add.problem.alreadyInstalled" | "panels.market.add.problem.notFound" | "panels.market.add.problem.notAPackage" | "panels.market.add.problem.notABundle" | "panels.market.add.problem.network" | "panels.market.add.problem.unknown" | "panels.market.add.reason.invalidSpec" | "panels.market.add.reason.incompatible" | "panels.market.add.reason.notBundle" | "panels.market.add.reason.notRemovable" | "panels.market.add.reason.bundleInUse" | "panels.market.add.reason.stopProfile" | "panels.market.add.reason.staleApproval" | "panels.market.add.reason.operationError" | "panels.market.add.reason.buildBlocked" | "panels.market.add.reason.pnpmMissing" | "panels.market.add.reason.noMatchingVersion" | "panels.market.add.reason.diskFull" | "panels.market.add.reason.permission" | "panels.market.add.reason.integrity" | "panels.market.add.reason.timeout" | "panels.market.add.reason.network" | "panels.market.add.reason.notFound" | "panels.market.add.reason.unknown" | "blocker.fix.running" | "blocker.fix.failed" | "blocker.retry.running" | "panels.reading" | "panels.cardBroken" | "panels.cardRetry" | "panels.empty" | "panels.emptyHint" | "env.title" | "env.checking" | "env.installing" | "env.failed" | "env.elapsed" | "env.stepFailed.install" | "env.stepFailed.path" | "env.stepFailed.shell" | "env.bridge.ok" | "env.bridge.short" | "env.bridge.why" | "env.bridge.daemon" | "env.bridge.extension" | "env.bridge.extensionLink" | "env.bridge.profile" | "env.bridge.stale" | "env.bridge.unknown" | "env.fix.title" | "env.fix.why" | "env.fix.whyPath" | "env.fix.whyShell" | "env.fix.step" | "env.fix.stepPath" | "env.fix.stepShell" | "env.fix.permission" | "env.fix.network" | "env.fix.details" | "env.retry" | "env.copyCommand" | "env.copied" | "env.step.opencli" | "env.step.codingPlugin" | "env.step.bridge" | "env.step.warm" | "env.warm.done" | "env.warm.short" | "rightbar.label" | "rightbar.expand" | "rightbar.collapse" | "rightbar.column.label" | "rightbar.column.workbench" | "rightbar.column.dsh" | "rightbar.refresh" | "rightbar.refreshing" | "rightbar.updatedAt" | "rightbar.noProject" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label", string>;
313
+ en: Record<"time.now" | "time.minutes" | "time.hours" | "time.days" | "blocker.retry" | "blocker.copy" | "blocker.unknown.title" | "blocker.login.go" | "blocker.login.titleGeneric" | "blocker.login.title" | "blocker.login.manual" | "blocker.login.hint" | "blocker.forbidden.open" | "blocker.forbidden.titleGeneric" | "blocker.forbidden.title" | "blocker.forbidden.hint" | "blocker.timeout.hint" | "blocker.exec.hint" | "blocker.fix.install" | "blocker.fix.update" | "blocker.market.open" | "env.unit.failed" | "env.unit.failedHint" | "env.unit.missingPlugin" | "env.unit.hint" | "env.unit.missingCommands" | "env.unit.missingCommandsHint" | "env.unit.unchecked" | "env.unit.uncheckedHint" | "brand.name" | "toggle.open" | "toggle.collapse" | "project.label" | "workspace.section" | "project.none" | "project.empty" | "project.add" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.rename.row" | "project.rename.current" | "project.rename.title" | "project.rename.description" | "project.rename.field" | "project.rename.placeholder" | "project.rename.hint" | "project.rename.save" | "project.rename.saving" | "project.rename.cancel" | "project.rename.failed" | "session.new" | "session.new.label" | "session.empty" | "session.noProject" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "panels.title" | "panels.reset" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.buildModule" | "panels.failed" | "panels.hintEffect" | "panels.dragHint" | "panels.unknownStep" | "panels.labelWithHint" | "panels.dep.failed" | "panels.dep.missingPlugin" | "panels.dep.missingCommands" | "panels.dep.unchecked" | "panels.tab.modules" | "panels.tab.market" | "panels.browseMarket" | "panels.noneInstalled" | "panels.noneInstalledHint" | "panels.market.installed" | "panels.market.outdated" | "panels.market.hasNew" | "panels.market.missing" | "panels.market.unchecked" | "panels.market.checking" | "panels.market.sectionInstalled" | "panels.market.sectionPending" | "panels.market.sectionShared" | "panels.market.sharedUsedBy" | "panels.market.listSeparator" | "panels.market.sharedMissing" | "panels.market.sharedOutdated" | "panels.market.sharedUnsatisfied" | "panels.market.sharedOk" | "panels.market.versionArrow" | "panels.market.lead" | "panels.market.install" | "panels.market.update" | "panels.market.uninstall" | "panels.market.uninstallLabel" | "panels.market.installing" | "panels.market.updating" | "panels.market.uninstalling" | "panels.market.confirmUninstallTitle" | "panels.market.confirmUninstallDetail" | "panels.market.confirmUninstallAck" | "panels.market.confirmYes" | "panels.market.confirmNo" | "panels.market.failed" | "panels.market.needEnv" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.installedSection" | "panels.market.moduleOutdated" | "panels.market.moduleRestartHint" | "panels.market.restartNeeded" | "panels.market.foot" | "panels.market.recheck" | "panels.market.checkUpdates" | "panels.market.checkedAt" | "panels.market.justNow" | "panels.market.minutesAgo" | "panels.market.hoursAgo" | "panels.market.add.description" | "panels.market.add.specPlaceholder" | "panels.market.add.guideToggle" | "panels.market.add.guideHide" | "panels.market.add.guideIdTitle" | "panels.market.add.guideIdExample" | "panels.market.add.guideIdHint" | "panels.market.add.guideGitTitle" | "panels.market.add.guideGitExample" | "panels.market.add.guideGitHint" | "panels.market.add.guidePathTitle" | "panels.market.add.guidePathExample" | "panels.market.add.guidePathHint" | "panels.market.add.guideExampleLabel" | "panels.market.add.guideFill" | "panels.market.add.guideSafety" | "panels.market.add.registryLegend" | "panels.market.add.registryOfficial" | "panels.market.add.registryMirror" | "panels.market.add.registryWithHost" | "panels.market.add.registryToggle" | "panels.market.add.installVersion" | "panels.market.add.registryDefault" | "panels.market.add.registryCustom" | "panels.market.add.registryCustomPlaceholder" | "panels.market.add.registryCustomHint" | "panels.market.add.registryCustomInvalid" | "panels.market.add.run" | "panels.market.add.edit" | "panels.market.add.changeRegistry" | "panels.market.add.installingTitle" | "panels.market.add.installedTitle" | "panels.market.add.failedTitle" | "panels.market.add.cancelledTitle" | "panels.market.add.failureNetworkHost" | "panels.market.add.problemInvalid" | "panels.market.add.problemInstalled" | "panels.market.add.problemNotFound" | "panels.market.add.problemNotPackage" | "panels.market.add.problemNotBundle" | "panels.market.add.problemNetwork" | "panels.market.add.problemUnknown" | "panels.market.add.failureNetwork" | "panels.market.add.failureNotFound" | "panels.market.add.failureNoMatchingVersion" | "panels.market.add.failureDiskFull" | "panels.market.add.failurePermission" | "panels.market.add.failureBuildBlocked" | "panels.market.add.failureIntegrity" | "panels.market.add.failureTimeout" | "panels.market.add.failurePnpmMissing" | "panels.market.add.failureGeneric" | "panels.market.add.approveTitle" | "panels.market.add.approveCaution" | "panels.market.add.approveRun" | "panels.market.add.restart" | "panels.market.add.tookEffect" | "panels.market.add.details" | "panels.market.add.open" | "panels.market.add.title" | "panels.market.add.intro" | "panels.market.add.specLabel" | "panels.market.add.specHint" | "panels.market.add.exampleTitle" | "panels.market.add.exampleFill" | "panels.market.add.registryLabel" | "panels.market.add.registryProfile" | "panels.market.add.registryPnpm" | "panels.market.add.check" | "panels.market.add.checking" | "panels.market.add.change" | "panels.market.add.previewTitle" | "panels.market.add.fieldName" | "panels.market.add.fieldVersion" | "panels.market.add.fieldDescription" | "panels.market.add.fieldSource" | "panels.market.add.install" | "panels.market.add.installing" | "panels.market.add.cancel" | "panels.market.add.close" | "panels.market.add.retry" | "panels.market.add.changeSource" | "panels.market.add.approveBuilds" | "panels.market.add.approveHint" | "panels.market.add.doneApplied" | "panels.market.add.doneRestart" | "panels.market.add.cancelled" | "panels.market.add.failed" | "panels.market.add.removed" | "panels.market.add.supplyPending" | "panels.market.add.supplyFailed" | "panels.market.add.notModule" | "panels.market.add.removedRestart" | "panels.market.add.remove" | "panels.market.add.problem.invalidSpec" | "panels.market.add.problem.alreadyInstalled" | "panels.market.add.problem.notFound" | "panels.market.add.problem.notAPackage" | "panels.market.add.problem.notABundle" | "panels.market.add.problem.network" | "panels.market.add.problem.unknown" | "panels.market.add.reason.invalidSpec" | "panels.market.add.reason.incompatible" | "panels.market.add.reason.notBundle" | "panels.market.add.reason.notRemovable" | "panels.market.add.reason.bundleInUse" | "panels.market.add.reason.stopProfile" | "panels.market.add.reason.staleApproval" | "panels.market.add.reason.operationError" | "panels.market.add.reason.buildBlocked" | "panels.market.add.reason.pnpmMissing" | "panels.market.add.reason.noMatchingVersion" | "panels.market.add.reason.diskFull" | "panels.market.add.reason.permission" | "panels.market.add.reason.integrity" | "panels.market.add.reason.timeout" | "panels.market.add.reason.network" | "panels.market.add.reason.notFound" | "panels.market.add.reason.unknown" | "blocker.fix.running" | "blocker.fix.failed" | "blocker.retry.running" | "panels.reading" | "panels.cardBroken" | "panels.cardRetry" | "panels.empty" | "panels.emptyHint" | "env.title" | "env.checking" | "env.installing" | "env.failed" | "env.elapsed" | "env.stepFailed.install" | "env.stepFailed.path" | "env.stepFailed.shell" | "env.bridge.ok" | "env.bridge.short" | "env.bridge.why" | "env.bridge.daemon" | "env.bridge.extension" | "env.bridge.extensionLink" | "env.bridge.profile" | "env.bridge.stale" | "env.bridge.unknown" | "env.fix.title" | "env.fix.why" | "env.fix.whyPath" | "env.fix.whyShell" | "env.fix.step" | "env.fix.stepPath" | "env.fix.stepShell" | "env.fix.permission" | "env.fix.network" | "env.fix.details" | "env.retry" | "env.copyCommand" | "env.copied" | "env.step.opencli" | "env.step.codingPlugin" | "env.step.bridge" | "env.step.warm" | "env.warm.done" | "env.warm.short" | "rightbar.label" | "rightbar.expand" | "rightbar.collapse" | "rightbar.column.label" | "rightbar.column.workbench" | "rightbar.column.dsh" | "rightbar.refresh" | "rightbar.refreshing" | "rightbar.updatedAt" | "rightbar.noProject" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label", string>;
313
314
  };
314
315
  export type { WorkbenchMessages };
315
316
  export interface Dictionaries {
package/dist/i18n/zh.d.ts CHANGED
@@ -44,6 +44,7 @@ export declare const zh: {
44
44
  readonly 'panels.enabled': "这个项目已启用";
45
45
  readonly 'panels.disabled': "这个项目未启用";
46
46
  readonly 'panels.choose': "选择模块";
47
+ readonly 'panels.buildModule': "造一个模块";
47
48
  readonly 'panels.failed': "改配置失败了,再试一次。";
48
49
  readonly 'panels.hintEffect': "停用 = agent 拿不到相关数据";
49
50
  readonly 'panels.dragHint': "拖动调整顺序";
package/dist/i18n/zh.js CHANGED
@@ -44,6 +44,7 @@ export const zh = {
44
44
  'panels.enabled': '这个项目已启用',
45
45
  'panels.disabled': '这个项目未启用',
46
46
  'panels.choose': '选择模块',
47
+ 'panels.buildModule': '造一个模块',
47
48
  'panels.failed': '改配置失败了,再试一次。',
48
49
  'panels.hintEffect': '停用 = agent 拿不到相关数据',
49
50
  'panels.dragHint': '拖动调整顺序',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -119,6 +119,7 @@ export interface DshWorkspaceWritten {
119
119
  }
120
120
 
121
121
  export interface DshWorkspaces {
122
+ readonly list: DshObservable<WorkspaceSnapshot>;
122
123
  create(request: { path: string }): Promise<DshWorkspaceWritten | undefined>;
123
124
  rename(workspaceId: string, title: string): Promise<DshWorkspaceWritten>;
124
125
  }
@@ -167,6 +168,12 @@ export interface DshConversationService {
167
168
  export interface DshSessionsService {
168
169
  readonly list: { getSnapshot(): SessionsSnapshot };
169
170
  scope(sessionId: string): unknown | undefined;
171
+ create?(request: {
172
+ readonly workspaceId?: string;
173
+ readonly sessionId?: string;
174
+ readonly cwd?: string;
175
+ readonly agentPreset?: string;
176
+ }): Promise<unknown>;
170
177
  }
171
178
 
172
179
  export interface DshObservable<T> {
@@ -182,6 +189,18 @@ export interface DshSidebarRight {
182
189
  openTab(kind: string, options?: unknown): void;
183
190
  }
184
191
 
192
+ export interface DshSessionsRemote {
193
+ create(request: {
194
+ readonly workspaceId?: string;
195
+ readonly cwd?: string;
196
+ readonly agentPreset?: string;
197
+ }): Promise<{ readonly sessionId: string }>;
198
+ }
199
+
200
+ export interface DshClientRemote {
201
+ readonly sessions: DshSessionsRemote;
202
+ }
203
+
185
204
  export interface DshClientContext {
186
205
  get<T = unknown>(name: string): T | undefined;
187
206
  provide(name: string, value: unknown): void;
@@ -192,4 +211,5 @@ export interface DshClientContext {
192
211
  readonly uiWorkspace: DshUiWorkspace;
193
212
  readonly workspaces: DshWorkspaces;
194
213
  readonly shortcuts: DshShortcutCatalog;
214
+ readonly remote: DshClientRemote;
195
215
  }
package/src/api/dsh.ts CHANGED
@@ -102,6 +102,7 @@ export interface DshHostContext {
102
102
  get<T = unknown>(name: string): T | undefined;
103
103
  provide(name: string, value: unknown): () => void;
104
104
  effect(effect: () => void | (() => void), label?: string): () => void;
105
+ inject(names: readonly string[], effect: (scoped: DshHostContext) => void): () => void;
105
106
  readonly logger?: DshLogger;
106
107
  readonly webServer: { register(route: DshWebRoute): () => void };
107
108
  }
@@ -130,8 +131,12 @@ export interface DshShell {
130
131
 
131
132
  export interface DshWorkspaceRegistry {
132
133
  get(id: string): { id: string; path?: string; title?: string } | undefined;
134
+ resolveByPath(path: string): Promise<{ readonly id: unknown } | undefined>;
135
+ create(path: string, title?: string): Promise<{ id?: string; path?: string; title?: string } | undefined>;
133
136
  }
134
137
 
138
+ export type DshHomePath = (relative: string) => string;
139
+
135
140
  export interface DshToolDefinition {
136
141
  name: string;
137
142
  description: string;
@@ -22,222 +22,76 @@ export interface ApprovalCall {
22
22
  readonly workspace?: string | undefined;
23
23
  }
24
24
 
25
+ export type ApprovalVerdict =
26
+ | { readonly kind: 'read' }
27
+ | {
28
+ readonly kind: 'ask';
29
+ readonly reason: string;
30
+ readonly question: ApprovalDetail;
31
+ readonly fallback: ApprovalDetail;
32
+ };
33
+
34
+ export type ApprovalPolicy = (call: ApprovalCall) => ApprovalVerdict | undefined;
35
+
25
36
  export interface ApprovalContext {
26
- readonly titleOf?: ((id: string) => string | undefined) | undefined;
27
37
  readonly writes?: boolean | undefined;
38
+ readonly policy?: ApprovalPolicy | undefined;
28
39
  readonly detailOf?:
29
40
  | ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>)
30
41
  | undefined;
31
42
  }
32
43
 
44
+ function messageOf(error: unknown): string {
45
+ return error instanceof Error ? error.message : String(error);
46
+ }
47
+
33
48
  function compose(
34
49
  detail: ApprovalDetail | undefined,
35
- texts: { readonly generic: ApprovalDetail; readonly question: ApprovalDetail },
50
+ verdict: { readonly question: ApprovalDetail; readonly fallback: ApprovalDetail },
36
51
  ): ApprovalDetail {
37
- if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '') return texts.generic;
38
- return { en: `${detail.en} — ${texts.question.en}`, zh: `${detail.zh} —— ${texts.question.zh}` };
39
- }
40
-
41
- function actionOf(call: ApprovalCall): { action?: string; id?: string } {
42
- const raw = call.arguments;
43
- if (typeof raw !== 'object' || raw === null) return {};
44
- const value = raw as { action?: unknown; id?: unknown; unit?: unknown };
52
+ if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '') {
53
+ return verdict.fallback;
54
+ }
45
55
  return {
46
- ...(typeof value.action === 'string' ? { action: value.action } : {}),
47
- ...(typeof value.id === 'string' ? { id: value.id } : typeof value.unit === 'string' ? { id: value.unit } : {}),
48
- };
49
- }
50
-
51
- const READ_ONLY_ACTIONS = new Set(['list', '']);
52
-
53
- interface AskSpec {
54
- readonly reason: string;
55
- readonly generic: ApprovalDetail;
56
- readonly question: ApprovalDetail;
57
- }
58
-
59
- type CallEffect =
60
- | { readonly kind: 'ask'; readonly spec: AskSpec }
61
- | { readonly kind: 'read' }
62
- | { readonly kind: 'unknown' };
63
-
64
- const READ: CallEffect = { kind: 'read' };
65
- const UNKNOWN: CallEffect = { kind: 'unknown' };
66
-
67
- function ask(spec: AskSpec): CallEffect {
68
- return { kind: 'ask', spec };
69
- }
70
-
71
- function confirmed(call: ApprovalCall): boolean | undefined {
72
- const raw = call.arguments;
73
- if (typeof raw !== 'object' || raw === null) return undefined;
74
- const value = (raw as { confirm?: unknown }).confirm;
75
- return typeof value === 'boolean' ? value : undefined;
76
- }
77
-
78
- function effectOf(call: ApprovalCall, context: ApprovalContext): CallEffect {
79
- const name = call.name ?? '';
80
- const { action, id } = actionOf(call);
81
- const readAction = action !== undefined && action !== '' && READ_ONLY_ACTIONS.has(action);
82
-
83
- const what = (): string => {
84
- if (id === undefined || id === '') return '一个模块';
85
- return `「${context.titleOf?.(id) ?? id}」`;
56
+ en: `${detail.en} — ${verdict.question.en}`,
57
+ zh: `${detail.zh} —— ${verdict.question.zh}`,
86
58
  };
87
-
88
- if (name === 'workbench_cmdb_release') {
89
- if (confirmed(call) !== true) return READ;
90
- return ask({
91
- reason: 'confirm=true releases a version to a real environment',
92
- generic: {
93
- en: 'The agent wants to deploy a version for real (confirm: true) — it deploys on the platform (non-production). Allow?',
94
- zh: 'agent 想在非生产环境上**真的部署一个版本**(confirm: true)—— 会在平台上部署。允许吗?',
95
- },
96
- question: {
97
- en: 'it deploys on the platform for real (non-production). Allow?',
98
- zh: '会在平台上真的部署一次(非生产环境)。允许吗?',
99
- },
100
- });
101
- }
102
-
103
- if (name === 'workbench_coding_issue_comment' || name === 'workbench_coding_issue_transfer') {
104
- if (confirmed(call) !== true) return READ;
105
- const comment = name === 'workbench_coding_issue_comment';
106
- return ask({
107
- reason: comment
108
- ? 'confirm=true posts a comment on the platform'
109
- : 'confirm=true moves an issue to another status on the platform',
110
- generic: comment
111
- ? {
112
- en: 'The agent wants to post a comment on the platform for real (confirm: true) — it writes on the platform. Allow?',
113
- zh: 'agent 想**真的给一条缺陷加评论**(confirm: true)—— 会写平台数据。允许吗?',
114
- }
115
- : {
116
- en: 'The agent wants to move an issue to another status for real (confirm: true) — it writes on the platform. Allow?',
117
- zh: 'agent 想**真的把一条缺陷流转到别的状态**(confirm: true)—— 会写平台数据。允许吗?',
118
- },
119
- question: comment
120
- ? {
121
- en: 'it posts that comment on the platform for real. Allow?',
122
- zh: '会在平台上真的发表这条评论。允许吗?',
123
- }
124
- : {
125
- en: 'it changes that issue status on the platform for real. Allow?',
126
- zh: '会在平台上真的改这条缺陷的状态。允许吗?',
127
- },
128
- });
129
- }
130
-
131
- if (name === 'workbench_modules') {
132
- if (readAction) return READ;
133
- if (action === 'update-dependency') {
134
- return ask({
135
- reason: `workbench_modules update-dependency ${id ?? ''} upgrades a shared dependency on this machine`,
136
- generic: {
137
- en: `The agent wants to upgrade the shared dependency ${what()} (it changes what is installed on this machine). Allow?`,
138
- zh: `agent 想升级共用依赖 ${what()}(会改这台机器上装的东西)。允许吗?`,
139
- },
140
- question: {
141
- en: 'it upgrades that shared dependency on this machine. Allow?',
142
- zh: `会升级共用依赖 ${what()}。允许吗?`,
143
- },
144
- });
145
- }
146
- const verb = action === 'install' ? '装' : action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '装 / 更新 / 卸载';
147
- return ask({
148
- reason: `workbench_modules ${action ?? '(unknown action)'} ${id ?? ''} changes plugins on this machine`.trim(),
149
- generic: {
150
- en: `The agent wants to ${verb === '装' ? 'install' : verb === '卸载' ? 'remove' : verb === '更新' ? 'update' : 'install/update/remove'} the module ${what()} (it changes plugins on this machine). Allow?`,
151
- zh: `agent 想${verb}模块 ${what()}(会改这台机器上的插件)。允许吗?`,
152
- },
153
- question: {
154
- en: 'it changes plugins on this machine. Allow?',
155
- zh: '会改这台机器上的插件。允许吗?',
156
- },
157
- });
158
- }
159
-
160
- if (name !== 'workbench_panels') return UNKNOWN;
161
-
162
- if (readAction) return READ;
163
-
164
- if (action === 'hide') {
165
- return ask({
166
- reason: 'workbench_panels hide removes a module from the panel',
167
- generic: {
168
- en: `The agent wants to remove ${what()} from this project's panel (its agent tools go offline here too). Allow?`,
169
- zh: `agent 想把 ${what()} 从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
170
- },
171
- question: {
172
- en: 'it takes that module off this panel (its agent tools go offline here too). Allow?',
173
- zh: `${what()}会从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
174
- },
175
- });
176
- }
177
- if (action === 'move') {
178
- return ask({
179
- reason: 'workbench_panels move reorders the panel',
180
- generic: {
181
- en: `The agent wants to reorder this project's panel (put ${what()} first). Allow?`,
182
- zh: `agent 想调整这个项目面板的顺序(把 ${what()} 放前面)。允许吗?`,
183
- },
184
- question: {
185
- en: `it reorders this panel (${what()} goes first). Allow?`,
186
- zh: `会调整这个项目面板的顺序(${what()}放前面)。允许吗?`,
187
- },
188
- });
189
- }
190
- if (action === 'reset') {
191
- return ask({
192
- reason: 'workbench_panels reset clears the panel',
193
- generic: {
194
- en: "The agent wants to clear this project's panel (every module goes offline here). Allow?",
195
- zh: 'agent 想清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
196
- },
197
- question: {
198
- en: 'it clears this panel (every module goes offline here). Allow?',
199
- zh: '会清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
200
- },
201
- });
202
- }
203
- return ask({
204
- reason: `workbench_panels ${action ?? '(unknown action)'} changes the user's panel`,
205
- generic: {
206
- en: `The agent wants to add ${what()} to this project's panel. Allow?`,
207
- zh: `agent 想把 ${what()} 加到**这个项目**的面板上。允许吗?`,
208
- },
209
- question: {
210
- en: `it adds ${what()} to this project's panel. Allow?`,
211
- zh: `会把 ${what()} 加到**这个项目**的面板上。允许吗?`,
212
- },
213
- });
214
59
  }
215
60
 
216
61
  export async function approvalFor(
217
62
  call: ApprovalCall,
218
63
  context: ApprovalContext = {},
219
64
  ): Promise<AskDecision | undefined> {
220
- const effect = effectOf(call, context);
221
- if (effect.kind === 'read') return undefined;
222
- if (effect.kind === 'unknown') {
223
- if (context.writes !== true) return undefined;
65
+ if (context.writes !== true) return undefined;
66
+
67
+ let verdict: ApprovalVerdict | undefined;
68
+ try {
69
+ verdict = context.policy?.(call);
70
+ } catch (error) {
71
+ return {
72
+ kind: 'deny',
73
+ reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf(error)}`,
74
+ };
75
+ }
76
+
77
+ if (verdict?.kind === 'read') return undefined;
78
+
79
+ if (verdict?.kind !== 'ask') {
224
80
  return {
225
81
  kind: 'deny',
226
82
  reason:
227
- `${call.name ?? '这把工具'} 在清单里声明会写东西,但审批表没认领这次调用 —— 停下:` +
228
- '给它在 tool-approval.ts 里补一条审批(含"这次会发生什么"),别让写静默过去',
83
+ `${call.name ?? '这把工具'} 在清单里声明会写东西,但它的套件没认领这次调用 —— 停下:` +
84
+ '在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去',
229
85
  };
230
86
  }
231
- const { spec } = effect;
232
87
 
233
88
  let facts: ApprovalFacts | undefined;
234
89
  try {
235
90
  facts = await context.detailOf?.(call);
236
91
  } catch (error) {
237
- const why = error instanceof Error ? error.message : String(error);
238
- return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${why}` };
92
+ return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${messageOf(error)}` };
239
93
  }
240
94
  if (facts !== undefined && 'blocked' in facts) return { kind: 'deny', reason: facts.blocked };
241
95
 
242
- return { kind: 'ask', reason: spec.reason, displayReason: compose(facts, spec) };
96
+ return { kind: 'ask', reason: verdict.reason, displayReason: compose(facts, verdict) };
243
97
  }
@@ -4,7 +4,7 @@ import type { RouteTable } from '../host/http.js';
4
4
  import type { LongTaskBase, LongTaskHost, LongTaskRegistry } from '../host/long-task.js';
5
5
  import type { ProvisionHandler } from '../host/provision.js';
6
6
  import type { UnitCapability } from './unit-libs.js';
7
- import type { ApprovalCall, ApprovalFacts } from '../host/tool-approval.js';
7
+ import type { ApprovalCall, ApprovalFacts, ApprovalPolicy } from '../host/tool-approval.js';
8
8
  import type { ValueHandoff } from '../host/value-handoff.js';
9
9
  import type { WorkspaceStore } from '../host/workspace-store.js';
10
10
 
@@ -57,6 +57,7 @@ export interface UnitMountResult {
57
57
  readonly approvalDetail?:
58
58
  | ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>)
59
59
  | undefined;
60
+ readonly needsApproval?: ApprovalPolicy | undefined;
60
61
  }
61
62
 
62
63
  export interface UnitHostFace {
package/src/i18n/en.ts CHANGED
@@ -49,6 +49,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
49
49
  'panels.enabled': 'enabled in this project',
50
50
  'panels.disabled': 'not enabled in this project',
51
51
  'panels.choose': 'Choose modules',
52
+ 'panels.buildModule': 'Build a module',
52
53
  'panels.failed': 'Could not save the change. Try again.',
53
54
  'panels.hintEffect': 'Disabling = the agent cannot reach that data',
54
55
  'panels.dragHint': 'Drag to reorder',
package/src/i18n/zh.ts CHANGED
@@ -47,6 +47,7 @@ export const zh = {
47
47
  'panels.enabled': '这个项目已启用',
48
48
  'panels.disabled': '这个项目未启用',
49
49
  'panels.choose': '选择模块',
50
+ 'panels.buildModule': '造一个模块',
50
51
  'panels.failed': '改配置失败了,再试一次。',
51
52
  'panels.hintEffect': '停用 = agent 拿不到相关数据',
52
53
  'panels.dragHint': '拖动调整顺序',