@tomflow/proflow-execution-browser-extension 0.1.12 → 0.1.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @tomflow/proflow-execution-browser-extension
2
2
 
3
+ ## 0.1.14
4
+
5
+ ### Patch Changes
6
+
7
+ - Retire the obsolete Carrier package and Browser standalone management CLI, make Platform setup/status preserve owner and dependency semantics, and enforce read-only status observation across the final Real-3 deployment closeout.
8
+
9
+ ## 0.1.13
10
+
11
+ ### Patch Changes
12
+
13
+ - Ship the Phase 3 browser, tunnel, model automation, platform readiness, and documentation closure as one verifiable platform snapshot.
14
+
3
15
  ## 0.1.12
4
16
 
5
17
  ### Patch Changes
package/DOCS.md CHANGED
@@ -6,13 +6,19 @@ Execution 领域拥有的 Chrome MV3 Extension,负责 Task UI、审批提醒
6
6
 
7
7
  ## 主要能力
8
8
 
9
- - 提供 New Task 与 Side Panel UI、Task Observer 和 System Observer
10
- - 创建、恢复和唤醒 Custom GPT Conversation,稳定观察 workerRef/c-id。
9
+ - 运行期提供 New Task 与 Side Panel UI、Task Observer、System Observer Browser Effect
10
+ - 运行期创建、恢复和唤醒 Custom GPT Conversation,稳定观察 workerRef/c-id。
11
+ - 部署期通过独立 `/gpts/editor` / `/gpts/editor/*` Provisioning Surface 物化 Custom GPT;该分支不进入 Task/Worker 状态机。
12
+ - 正常创建路径在单一 GPT Editor 会话中完成字段、Action Schema、API Key/Bearer、Knowledge ZIP、模型与 Capabilities 配置,再创建 Private GPT;不在 Create 后重新打开同一 GPT 做 Auth。
13
+ - Knowledge Bundle 在 Mac 侧先做 ZIP traversal/大小/内部类型校验,最终向 GPT Knowledge 上传 `custom-gpt-knowledge.zip` 本体;不把解包后的 `.md` 逐文件作为正常上传物。
14
+ - `FINALIZE_CUSTOM_GPT_AUTH` 只保留为显式 recovery/repair primitive,不属于正常 `createCustomGptRole` Golden Path。
11
15
  - 执行 DOM-first 浏览器操作,必要时使用截图与 Vision 辅助观察。
12
16
 
13
17
  ## 提供的 API 与 Public Contract
14
18
 
15
- - 提供 `execution-browser-executor` Contract,版本 `1.0.0`。
19
+ - 提供 `execution-browser-executor` Contract,版本 `1.0.0`,仅用于运行期 Browser Execution。
20
+ - 提供 `custom-gpt-web-provisioning` Contract,版本 `1.0.0`,仅用于部署期 Custom GPT Web Provisioning。
21
+ - 两条能力只共享 Extension/loopback transport 等底层设施,不共享业务状态机。
16
22
  - 通过本地 Bridge 接收 typed Browser operation 并返回 Evidence。
17
23
 
18
24
  ## 依赖的 Module、Contract 和外部资源
@@ -26,7 +32,9 @@ Execution 领域拥有的 Chrome MV3 Extension,负责 Task UI、审批提醒
26
32
 
27
33
  ## 使用方式
28
34
 
29
- 运行 `pnpm exec -- proflow-execution-browser-extension setup`,加载准备好的目录、登记 Extension IDReload 并验证 Bridge
35
+ 运行 `platform setup --workspace <workspace> --module execution-browser-extension`。ProFlow 会先准备 unpacked 目录、运行配置和本地 Bridge,并用真实 hello + heartbeat 尝试重验证已有扩展;已有扩展仍在线时不会再次打扰用户。只有未检测到在线扩展时,ProFlow 才把正确目录复制到剪贴板并打开 `chrome://extensions/`,提示用户完成安装操作:如果开发者模式尚未开启则先开启,然后点击“加载未打包的扩展程序”,在目录选择窗口中粘贴并确认目录。完成后无需回终端输入任何标识或状态;扩展身份、sessionhello/heartbeat、Browser Executor 配置和 setup evidence 都由机器自动发现、生成并验证。历史 pairing evidence 不能单独代替本次 live revalidation
36
+
37
+ 若 pairing 等待超时,ProFlow 保留已经准备好的扩展文件和机器配置,不写伪 READY evidence,并输出稳定错误代码 `PAIRING_TIMEOUT`。用户确认 Chrome 已完成加载后重新执行 `platform setup` 即可。Agent setup 随后通过 `custom-gpt-web-provisioning` 使用同一 Extension package 的部署期分支。
30
38
 
31
39
  ## 职责边界与限制
32
40
 
package/SETUP.md CHANGED
@@ -1,25 +1,21 @@
1
1
  # execution-browser-extension Setup
2
2
 
3
- ## STEP-EXECUTION-BROWSER-EXTENSION-01 — 准备扩展并打开 Chrome
3
+ ## STEP-EXECUTION-BROWSER-EXTENSION-01 — 加载扩展并自动配对
4
4
  Responsible: USER
5
- Interactive executable: `pnpm exec -- proflow-execution-browser-extension setup 01`
6
- Non-interactive executable: `pnpm exec -- proflow-execution-browser-extension setup 01`
5
+ Interactive executable: `platform setup --module execution-browser-extension`
6
+ Non-interactive executable: `platform setup --module execution-browser-extension`
7
7
  Required inputs: none
8
- Verify: `pnpm exec -- proflow-execution-browser-extension setup 01`
9
- Success condition: 扩展目录已生成并可选择。
8
+ Verify: `platform status`
9
+ Success condition: 本地 pairing listener 已收到真实 Extension hello + heartbeat,并持久化部署证据。
10
10
 
11
- ## STEP-EXECUTION-BROWSER-EXTENSION-02 登记 Extension ID 并生成配置
12
- Responsible: USER
13
- Interactive executable: `pnpm exec -- proflow-execution-browser-extension setup 02`
14
- Non-interactive executable: `pnpm exec -- proflow-execution-browser-extension setup 02 --extension-id <id>`
15
- Required inputs: Chrome Extension ID
16
- Verify: `pnpm exec -- proflow-execution-browser-extension verify`
17
- Success condition: Extension ID 和运行配置已保存。
11
+ 运行命令后,ProFlow 会先准备 unpacked 目录和所有机器配置,并尝试用真实 hello + heartbeat 重验证已有扩展。若已有扩展仍在线,则无需任何人工操作,setup 直接继续;只有未检测到在线扩展时,ProFlow 才把正确目录复制到剪贴板、打开 `chrome://extensions/` 并提示用户完成安装操作:如果开发者模式尚未开启则先开启,然后点击“加载未打包的扩展程序”,在目录选择窗口中粘贴并确认刚刚复制的目录。完成后无需回终端输入任何内容;扩展身份确认、Bridge 配置、hello/heartbeat 验证和 READY 状态均由 ProFlow 自动完成。历史 `setup.json` / verification evidence 不能单独跳过本次 live revalidation。
12
+
13
+ 若等待超时,已经准备好的扩展文件和机器配置会保留,且不会写入伪 READY evidence。用户只需确认 Chrome 中已经完成扩展加载,然后重新执行 `platform setup`;稳定错误代码为 `PAIRING_TIMEOUT`。
18
14
 
19
- ## STEP-EXECUTION-BROWSER-EXTENSION-03验证 Service Worker 与 Bridge
15
+ ## STEP-EXECUTION-BROWSER-EXTENSION-02验证部署状态
20
16
  Responsible: AI
21
- Interactive executable: `pnpm exec -- proflow-execution-browser-extension setup 03`
22
- Non-interactive executable: `pnpm exec -- proflow-execution-browser-extension verify`
17
+ Interactive executable: `platform status`
18
+ Non-interactive executable: `platform status`
23
19
  Required inputs: none
24
- Verify: `pnpm exec -- proflow-execution-browser-extension verify`
20
+ Verify: `platform status`
25
21
  Success condition: `execution-browser-extension.setupStatus=READY`.
@@ -7,6 +7,16 @@ export declare function materializeProductionConfig(input: {
7
7
  }): Promise<{
8
8
  loadDir: string;
9
9
  }>;
10
+ export declare function pairBrowserExtensionSetup(context: ModuleCommandContext, options?: {
11
+ timeoutMs?: number;
12
+ onWaiting?: (input: {
13
+ loadDir: string;
14
+ endpoint: string;
15
+ }) => void | Promise<void>;
16
+ }): Promise<{
17
+ extensionId: string;
18
+ extensionInstanceId: string;
19
+ }>;
10
20
  export declare const behaviorAdapter: {
11
21
  readonly install: (context: ModuleCommandContext) => Promise<{
12
22
  result: {
@@ -14,7 +24,7 @@ export declare const behaviorAdapter: {
14
24
  ok: true;
15
25
  status: "SUCCEEDED";
16
26
  moduleRef: "execution-browser-extension";
17
- moduleVersion: "0.1.12";
27
+ moduleVersion: "0.1.14";
18
28
  data: {
19
29
  loadDir: string;
20
30
  };
@@ -27,7 +37,7 @@ export declare const behaviorAdapter: {
27
37
  readonly ok: true;
28
38
  readonly status: "SUCCEEDED";
29
39
  readonly moduleRef: "execution-browser-extension";
30
- readonly moduleVersion: "0.1.12";
40
+ readonly moduleVersion: "0.1.14";
31
41
  };
32
42
  observedEffects: never[];
33
43
  }>;
@@ -37,10 +47,10 @@ export declare const behaviorAdapter: {
37
47
  ok: true;
38
48
  status: "SUCCEEDED";
39
49
  moduleRef: "execution-browser-extension";
40
- moduleVersion: "0.1.12";
50
+ moduleVersion: "0.1.14";
41
51
  data: {
42
52
  setupStatus: "ACTION_REQUIRED" | "READY";
43
- runtimeStatus: "RUNNING" | "STOPPED";
53
+ runtimeStatus: "NOT_APPLICABLE";
44
54
  issues?: {
45
55
  scope: "SETUP";
46
56
  code: string;
@@ -53,10 +63,19 @@ export declare const behaviorAdapter: {
53
63
  observedEffects: string[];
54
64
  }>;
55
65
  readonly setup: (context: ModuleCommandContext) => Promise<{
66
+ result: {
67
+ readonly contract: "deployment.result.v1";
68
+ readonly ok: true;
69
+ readonly status: "SUCCEEDED";
70
+ readonly moduleRef: "execution-browser-extension";
71
+ readonly moduleVersion: "0.1.14";
72
+ };
73
+ observedEffects: string[];
74
+ } | {
56
75
  result: {
57
76
  contract: "deployment.result.v1";
58
77
  moduleRef: "execution-browser-extension";
59
- moduleVersion: "0.1.12";
78
+ moduleVersion: "0.1.14";
60
79
  ok: false;
61
80
  status: "FAILED";
62
81
  error: {
@@ -66,92 +85,22 @@ export declare const behaviorAdapter: {
66
85
  };
67
86
  data: {
68
87
  readonly steps: readonly [{
69
- readonly id: "STEP-EXECUTION-BROWSER-EXTENSION-04";
70
- readonly title: "修复扩展配置";
88
+ readonly id: "STEP-EXECUTION-BROWSER-EXTENSION-RECOVER";
89
+ readonly title: "重新加载并自动配对扩展";
71
90
  readonly state: "BLOCKED";
72
91
  readonly responsible: "EXTERNAL";
73
92
  readonly execution: {
74
- readonly interactive: "pnpm exec -- proflow-execution-browser-extension setup";
75
- readonly nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup --extension-id <id>";
93
+ readonly interactive: "platform setup --module execution-browser-extension";
94
+ readonly nonInteractive: "platform setup --module execution-browser-extension";
76
95
  };
77
96
  readonly requiredInputs: readonly [];
78
- readonly verify: "pnpm exec -- proflow-execution-browser-extension verify";
97
+ readonly verify: "platform status";
79
98
  readonly successCondition: "配置状态变为“已就绪”";
80
- readonly blockedReason: "Extension ID 或生成配置无效";
81
- }];
82
- };
83
- };
84
- observedEffects: never[];
85
- } | {
86
- result: {
87
- contract: "deployment.result.v1";
88
- moduleRef: "execution-browser-extension";
89
- moduleVersion: "0.1.12";
90
- ok: false;
91
- status: "ACTION_REQUIRED";
92
- data: {
93
- readonly steps: readonly [{
94
- readonly id: "STEP-EXECUTION-BROWSER-EXTENSION-01";
95
- readonly title: "准备扩展并打开 Chrome";
96
- readonly description: "生成 unpacked 目录,复制路径并打开扩展管理页。";
97
- readonly state: "TODO";
98
- readonly responsible: "USER";
99
- readonly execution: {
100
- readonly interactive: "pnpm exec -- proflow-execution-browser-extension setup 01";
101
- readonly nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 01";
102
- };
103
- readonly requiredInputs: readonly [];
104
- readonly verify: "pnpm exec -- proflow-execution-browser-extension setup 01";
105
- readonly successCondition: "扩展目录已生成并可选择";
106
- readonly humanAction: "启用开发者模式并选择脚本复制的目录。";
107
- }, {
108
- readonly id: "STEP-EXECUTION-BROWSER-EXTENSION-02";
109
- readonly title: "登记 Extension ID 并生成配置";
110
- readonly description: "保存扩展 ID,生成 Bridge 配置并提示 Reload。";
111
- readonly state: "TODO";
112
- readonly responsible: "USER";
113
- readonly execution: {
114
- readonly interactive: "pnpm exec -- proflow-execution-browser-extension setup 02";
115
- readonly nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 02 --extension-id <id>";
116
- };
117
- readonly requiredInputs: readonly [{
118
- readonly name: "extensionId";
119
- readonly description: "Chrome Extension ID";
120
- readonly sensitive: false;
121
- }];
122
- readonly verify: "pnpm exec -- proflow-execution-browser-extension verify";
123
- readonly successCondition: "Extension ID 和运行配置已保存";
124
- readonly humanAction: "复制扩展 ID,并在配置生成后点击 Reload。";
125
- }, {
126
- readonly id: "STEP-EXECUTION-BROWSER-EXTENSION-03";
127
- readonly title: "验证 Service Worker 与 Bridge";
128
- readonly description: "重新观察扩展后台和本地 Bridge。";
129
- readonly state: "TODO";
130
- readonly responsible: "AI";
131
- readonly execution: {
132
- readonly interactive: "pnpm exec -- proflow-execution-browser-extension setup 03";
133
- readonly nonInteractive: "pnpm exec -- proflow-execution-browser-extension verify";
134
- };
135
- readonly requiredInputs: readonly [];
136
- readonly verify: "pnpm exec -- proflow-execution-browser-extension verify";
137
- readonly successCondition: "execution-browser-extension.setupStatus=READY";
99
+ readonly blockedReason: "真实 Extension heartbeat 未到达或本地 pairing 失败";
138
100
  }];
139
101
  };
140
- actionRequired: {
141
- action: string;
142
- description: string;
143
- };
144
102
  };
145
103
  observedEffects: never[];
146
- } | {
147
- result: {
148
- readonly contract: "deployment.result.v1";
149
- readonly ok: true;
150
- readonly status: "SUCCEEDED";
151
- readonly moduleRef: "execution-browser-extension";
152
- readonly moduleVersion: "0.1.12";
153
- };
154
- observedEffects: string[];
155
104
  }>;
156
105
  readonly docs: (_context: ModuleCommandContext) => Promise<{
157
106
  result: {
@@ -159,7 +108,7 @@ export declare const behaviorAdapter: {
159
108
  ok: true;
160
109
  status: "SUCCEEDED";
161
110
  moduleRef: "execution-browser-extension";
162
- moduleVersion: "0.1.12";
111
+ moduleVersion: "0.1.14";
163
112
  data: {
164
113
  docs: string;
165
114
  };
@@ -172,11 +121,11 @@ export declare const behaviorAdapter: {
172
121
  readonly ok: true;
173
122
  readonly status: "SUCCEEDED";
174
123
  readonly moduleRef: "execution-browser-extension";
175
- readonly moduleVersion: "0.1.12";
124
+ readonly moduleVersion: "0.1.14";
176
125
  } | {
177
126
  contract: "deployment.result.v1";
178
127
  moduleRef: "execution-browser-extension";
179
- moduleVersion: "0.1.12";
128
+ moduleVersion: "0.1.14";
180
129
  ok: false;
181
130
  status: "FAILED";
182
131
  error: {
@@ -193,7 +142,7 @@ export declare const behaviorAdapter: {
193
142
  readonly ok: true;
194
143
  readonly status: "SUCCEEDED";
195
144
  readonly moduleRef: "execution-browser-extension";
196
- readonly moduleVersion: "0.1.12";
145
+ readonly moduleVersion: "0.1.14";
197
146
  };
198
147
  observedEffects: never[];
199
148
  }>;
@@ -3,6 +3,8 @@ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { deterministicLoopbackPort, ensureModuleSecretFile, moduleWorkspaceStateDirectory, readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
6
+ import { runInteractiveBrowserExtensionSetup } from "../src/install-workflow.js";
7
+ import { createBrowserExtensionPairingServer } from "../src/pairing.js";
6
8
  import { descriptor } from "./descriptor.js";
7
9
  const base = {
8
10
  contract: "deployment.result.v1",
@@ -11,75 +13,21 @@ const base = {
11
13
  moduleRef: descriptor.moduleRef,
12
14
  moduleVersion: descriptor.moduleVersion,
13
15
  };
14
- const setupPlan = {
15
- steps: [
16
- {
17
- id: "STEP-EXECUTION-BROWSER-EXTENSION-01",
18
- title: "准备扩展并打开 Chrome",
19
- description: "生成 unpacked 目录,复制路径并打开扩展管理页。",
20
- state: "TODO",
21
- responsible: "USER",
22
- execution: {
23
- interactive: "pnpm exec -- proflow-execution-browser-extension setup 01",
24
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 01",
25
- },
26
- requiredInputs: [],
27
- verify: "pnpm exec -- proflow-execution-browser-extension setup 01",
28
- successCondition: "扩展目录已生成并可选择",
29
- humanAction: "启用开发者模式并选择脚本复制的目录。",
30
- },
31
- {
32
- id: "STEP-EXECUTION-BROWSER-EXTENSION-02",
33
- title: "登记 Extension ID 并生成配置",
34
- description: "保存扩展 ID,生成 Bridge 配置并提示 Reload。",
35
- state: "TODO",
36
- responsible: "USER",
37
- execution: {
38
- interactive: "pnpm exec -- proflow-execution-browser-extension setup 02",
39
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 02 --extension-id <id>",
40
- },
41
- requiredInputs: [
42
- {
43
- name: "extensionId",
44
- description: "Chrome Extension ID",
45
- sensitive: false,
46
- },
47
- ],
48
- verify: "pnpm exec -- proflow-execution-browser-extension verify",
49
- successCondition: "Extension ID 和运行配置已保存",
50
- humanAction: "复制扩展 ID,并在配置生成后点击 Reload。",
51
- },
52
- {
53
- id: "STEP-EXECUTION-BROWSER-EXTENSION-03",
54
- title: "验证 Service Worker 与 Bridge",
55
- description: "重新观察扩展后台和本地 Bridge。",
56
- state: "TODO",
57
- responsible: "AI",
58
- execution: {
59
- interactive: "pnpm exec -- proflow-execution-browser-extension setup 03",
60
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension verify",
61
- },
62
- requiredInputs: [],
63
- verify: "pnpm exec -- proflow-execution-browser-extension verify",
64
- successCondition: "execution-browser-extension.setupStatus=READY",
65
- },
66
- ],
67
- };
68
16
  const blockedSetupPlan = {
69
17
  steps: [
70
18
  {
71
- id: "STEP-EXECUTION-BROWSER-EXTENSION-04",
72
- title: "修复扩展配置",
19
+ id: "STEP-EXECUTION-BROWSER-EXTENSION-RECOVER",
20
+ title: "重新加载并自动配对扩展",
73
21
  state: "BLOCKED",
74
22
  responsible: "EXTERNAL",
75
23
  execution: {
76
- interactive: "pnpm exec -- proflow-execution-browser-extension setup",
77
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup --extension-id <id>",
24
+ interactive: "platform setup --module execution-browser-extension",
25
+ nonInteractive: "platform setup --module execution-browser-extension",
78
26
  },
79
27
  requiredInputs: [],
80
- verify: "pnpm exec -- proflow-execution-browser-extension verify",
28
+ verify: "platform status",
81
29
  successCondition: "配置状态变为“已就绪”",
82
- blockedReason: "Extension ID 或生成配置无效",
30
+ blockedReason: "真实 Extension heartbeat 未到达或本地 pairing 失败",
83
31
  },
84
32
  ],
85
33
  };
@@ -176,7 +124,10 @@ async function readEvidence(context, loadDir) {
176
124
  raw.loadDir === loadDir &&
177
125
  typeof raw.extensionId === "string" &&
178
126
  /^[a-z]{32}$/.test(raw.extensionId) &&
127
+ typeof raw.extensionInstanceId === "string" &&
128
+ raw.extensionInstanceId.length > 0 &&
179
129
  raw.serviceWorker === "RUNNING" &&
130
+ raw.evidenceSource === "PAIRING_HEARTBEAT" &&
180
131
  typeof raw.observedAt === "string" &&
181
132
  !Number.isNaN(Date.parse(raw.observedAt))
182
133
  ? raw
@@ -186,28 +137,19 @@ async function readEvidence(context, loadDir) {
186
137
  return undefined;
187
138
  }
188
139
  }
189
- function input(context) {
190
- const value = context.input;
191
- if (typeof value !== "object" || value === null || Array.isArray(value))
192
- return {};
193
- const extensionId = Reflect.get(value, "extensionId"), serviceWorker = Reflect.get(value, "serviceWorker"), observedAt = Reflect.get(value, "observedAt");
194
- return {
195
- ...(typeof extensionId === "string" ? { extensionId } : {}),
196
- ...(serviceWorker === "RUNNING"
197
- ? { serviceWorker: "RUNNING" }
198
- : {}),
199
- ...(typeof observedAt === "string" ? { observedAt } : {}),
200
- };
201
- }
202
140
  async function ownFacts(context) {
203
141
  await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
204
142
  const loadDir = browserExtensionLoadDir(context.workspaceRoot);
205
143
  const bridgeTokenFile = await ensureModuleSecretFile(context, descriptor.moduleRef, "bridge");
206
144
  const bridgePort = deterministicLoopbackPort(context, descriptor.moduleRef, "bridge");
145
+ const provisioningBridgeTokenFile = await ensureModuleSecretFile(context, descriptor.moduleRef, "provisioning");
146
+ const provisioningBridgePort = deterministicLoopbackPort(context, descriptor.moduleRef, "provisioning");
207
147
  const facts = {
208
148
  loadDir,
209
149
  bridgeTokenFile,
210
150
  bridgeEndpoint: `http://127.0.0.1:${bridgePort}`,
151
+ provisioningBridgeTokenFile,
152
+ provisioningBridgeEndpoint: `http://127.0.0.1:${provisioningBridgePort}`,
211
153
  verificationEvidenceFile: verificationFile(context),
212
154
  };
213
155
  const setup = await readSetup(context);
@@ -221,7 +163,7 @@ async function ownFacts(context) {
221
163
  await writeModuleSharedFacts(context, descriptor.moduleRef, facts);
222
164
  return facts;
223
165
  }
224
- async function materialize(context, setup) {
166
+ async function materializeRuntimeConfig(context) {
225
167
  const host = await readModuleSharedFacts(context, "platform-host");
226
168
  const endpoint = factString(host, "endpoint"), taskTokenFile = factString(host, "taskApplicationTokenFile"), approvalTokenFile = factString(host, "approvalApplicationTokenFile");
227
169
  if (!endpoint || !taskTokenFile || !approvalTokenFile)
@@ -230,11 +172,17 @@ async function materialize(context, setup) {
230
172
  const loadDir = String(facts.loadDir);
231
173
  const bridgeTokenFile = String(facts.bridgeTokenFile);
232
174
  const bridgeEndpoint = String(facts.bridgeEndpoint);
233
- const runtimeConfig = {
175
+ const provisioningBridgeTokenFile = String(facts.provisioningBridgeTokenFile);
176
+ const provisioningBridgeEndpoint = String(facts.provisioningBridgeEndpoint);
177
+ await writeFile(join(loadDir, "runtime-config.json"), `${JSON.stringify({
234
178
  proflowRuntimeBridge: {
235
179
  endpoint: bridgeEndpoint,
236
180
  token: await credential(bridgeTokenFile),
237
181
  },
182
+ proflowProvisioningBridge: {
183
+ endpoint: provisioningBridgeEndpoint,
184
+ token: await credential(provisioningBridgeTokenFile),
185
+ },
238
186
  proflowTaskApplication: {
239
187
  endpoint,
240
188
  token: await credential(taskTokenFile),
@@ -243,15 +191,87 @@ async function materialize(context, setup) {
243
191
  endpoint,
244
192
  token: await credential(approvalTokenFile),
245
193
  },
194
+ }, null, 2)}\n`, { mode: 0o600 });
195
+ return {
196
+ facts,
197
+ loadDir,
198
+ bridgeTokenFile,
199
+ bridgeEndpoint,
200
+ provisioningBridgeTokenFile,
201
+ provisioningBridgeEndpoint,
202
+ endpoint,
203
+ taskTokenFile,
246
204
  };
247
- await writeFile(join(loadDir, "runtime-config.json"), `${JSON.stringify(runtimeConfig, null, 2)}\n`, { mode: 0o600 });
248
- await writeFile(executorConfigFile(context), `${JSON.stringify({ platformHost: { endpoint, tokenFile: taskTokenFile }, bridge: { extensionId: setup.extensionId, tokenFile: bridgeTokenFile, host: "127.0.0.1", port: Number(new URL(bridgeEndpoint).port) } }, null, 2)}\n`, { mode: 0o600 });
205
+ }
206
+ async function materializeExecutorConfig(context, setup, prepared) {
207
+ const resolved = prepared ?? (await materializeRuntimeConfig(context));
208
+ await writeFile(executorConfigFile(context), `${JSON.stringify({ platformHost: { endpoint: resolved.endpoint, tokenFile: resolved.taskTokenFile }, bridge: { extensionId: setup.extensionId, tokenFile: resolved.bridgeTokenFile, host: "127.0.0.1", port: Number(new URL(resolved.bridgeEndpoint).port) } }, null, 2)}\n`, { mode: 0o600 });
249
209
  await writeModuleSharedFacts(context, descriptor.moduleRef, {
250
- ...facts,
210
+ ...resolved.facts,
251
211
  extensionId: setup.extensionId,
252
212
  browserExecutorConfigPath: executorConfigFile(context),
253
213
  });
254
214
  }
215
+ async function persistPairedBrowserExtension(context, prepared, identity) {
216
+ const setup = { extensionId: identity.extensionId };
217
+ await writeFile(setupFile(context), `${JSON.stringify(setup, null, 2)}\n`, {
218
+ mode: 0o600,
219
+ });
220
+ await materializeExecutorConfig(context, setup, prepared);
221
+ const evidence = {
222
+ contract: "proflow.browser-extension-verification.v1",
223
+ moduleVersion: descriptor.moduleVersion,
224
+ loadDir: prepared.loadDir,
225
+ extensionId: identity.extensionId,
226
+ extensionInstanceId: identity.extensionInstanceId,
227
+ serviceWorker: "RUNNING",
228
+ evidenceSource: "PAIRING_HEARTBEAT",
229
+ observedAt: new Date().toISOString(),
230
+ };
231
+ await writeFile(verificationFile(context), `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
232
+ return identity;
233
+ }
234
+ export async function pairBrowserExtensionSetup(context, options = {}) {
235
+ await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
236
+ await installPackage(context);
237
+ const existingSetup = await readSetup(context);
238
+ const existingEvidence = await readEvidence(context, browserExtensionLoadDir(context.workspaceRoot));
239
+ const canRevalidateExisting = Boolean(existingSetup &&
240
+ existingEvidence &&
241
+ existingSetup.extensionId === existingEvidence.extensionId);
242
+ const prepared = await materializeRuntimeConfig(context);
243
+ const pairing = await createBrowserExtensionPairingServer({
244
+ token: await credential(prepared.bridgeTokenFile),
245
+ host: "127.0.0.1",
246
+ port: Number(new URL(prepared.bridgeEndpoint).port),
247
+ pairingTimeoutMs: options.timeoutMs ?? 120_000,
248
+ });
249
+ try {
250
+ const pairingPromise = pairing.waitForPairing();
251
+ if (canRevalidateExisting) {
252
+ const timeoutMs = options.timeoutMs ?? 120_000;
253
+ const revalidationGraceMs = Math.min(2_000, Math.max(25, Math.floor(timeoutMs / 4)));
254
+ const revalidated = await Promise.race([
255
+ pairingPromise.then((identity) => ({
256
+ kind: "PAIRED",
257
+ identity,
258
+ })),
259
+ new Promise((resolve) => setTimeout(() => resolve({ kind: "WAITING" }), revalidationGraceMs)),
260
+ ]);
261
+ if (revalidated.kind === "PAIRED") {
262
+ return persistPairedBrowserExtension(context, prepared, revalidated.identity);
263
+ }
264
+ }
265
+ await options.onWaiting?.({
266
+ loadDir: prepared.loadDir,
267
+ endpoint: pairing.endpoint,
268
+ });
269
+ return persistPairedBrowserExtension(context, prepared, await pairingPromise);
270
+ }
271
+ finally {
272
+ await pairing.close();
273
+ }
274
+ }
255
275
  const failed = (code, message, retryable = true) => ({
256
276
  ...base,
257
277
  ok: false,
@@ -274,7 +294,7 @@ export const behaviorAdapter = {
274
294
  const loadDir = browserExtensionLoadDir(context.workspaceRoot);
275
295
  const setup = await readSetup(context);
276
296
  const evidence = await readEvidence(context, loadDir);
277
- const setupReady = Boolean(setup && evidence);
297
+ const setupReady = Boolean(setup && evidence && setup.extensionId === evidence.extensionId);
278
298
  return {
279
299
  result: {
280
300
  ...base,
@@ -282,7 +302,7 @@ export const behaviorAdapter = {
282
302
  setupStatus: setupReady
283
303
  ? "READY"
284
304
  : "ACTION_REQUIRED",
285
- runtimeStatus: evidence ? "RUNNING" : "STOPPED",
305
+ runtimeStatus: "NOT_APPLICABLE",
286
306
  ...(setupReady
287
307
  ? {}
288
308
  : {
@@ -304,77 +324,38 @@ export const behaviorAdapter = {
304
324
  };
305
325
  },
306
326
  setup: async (context) => {
307
- await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
308
- const supplied = input(context);
309
- let setup = await readSetup(context);
310
- if (supplied.extensionId) {
311
- if (!/^[a-z]{32}$/.test(supplied.extensionId))
312
- return {
313
- result: {
314
- ...failed("SETUP_FAILED", "extensionId must be a canonical 32-character Chromium extension id", false),
315
- data: blockedSetupPlan,
316
- },
317
- observedEffects: [],
318
- };
319
- setup = { extensionId: supplied.extensionId };
320
- await writeFile(setupFile(context), `${JSON.stringify(setup, null, 2)}\n`, { mode: 0o600 });
321
- }
322
- if (!setup)
327
+ try {
328
+ const input = typeof context.input === "object" && context.input !== null
329
+ ? context.input
330
+ : undefined;
331
+ await runInteractiveBrowserExtensionSetup({
332
+ workspaceRoot: context.workspaceRoot,
333
+ ...(input?.timeoutMs === undefined
334
+ ? {}
335
+ : { timeoutMs: input.timeoutMs }),
336
+ ...(input?.desktop === undefined ? {} : { desktop: input.desktop }),
337
+ ...(input?.pair === undefined ? {} : { pair: input.pair }),
338
+ });
323
339
  return {
324
- result: {
325
- ...base,
326
- ok: false,
327
- status: "ACTION_REQUIRED",
328
- data: setupPlan,
329
- actionRequired: {
330
- action: "load-unpacked-extension",
331
- description: `Load ${browserExtensionLoadDir(context.workspaceRoot)} in Chrome, copy its extensionId, then run proflow-execution-browser-extension setup --extension-id <id>.`,
332
- },
333
- },
334
- observedEffects: [],
340
+ result: base,
341
+ observedEffects: [
342
+ "Materializes browser runtime configuration",
343
+ "Opens Chrome extension management",
344
+ "Persists heartbeat-proven Chrome MV3 pairing evidence",
345
+ ],
335
346
  };
336
- try {
337
- await materialize(context, setup);
338
347
  }
339
348
  catch (error) {
340
349
  return {
341
350
  result: {
342
351
  ...failed("SETUP_FAILED", error instanceof Error
343
352
  ? error.message
344
- : "browser extension config materialization failed"),
353
+ : "browser extension setup failed"),
345
354
  data: blockedSetupPlan,
346
355
  },
347
356
  observedEffects: [],
348
357
  };
349
358
  }
350
- if (supplied.serviceWorker === "RUNNING") {
351
- const evidence = {
352
- contract: "proflow.browser-extension-verification.v1",
353
- moduleVersion: descriptor.moduleVersion,
354
- loadDir: browserExtensionLoadDir(context.workspaceRoot),
355
- extensionId: setup.extensionId,
356
- serviceWorker: "RUNNING",
357
- observedAt: supplied.observedAt ?? new Date().toISOString(),
358
- };
359
- await writeFile(verificationFile(context), `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
360
- return {
361
- result: base,
362
- observedEffects: ["Records real Chrome MV3 load evidence"],
363
- };
364
- }
365
- return {
366
- result: {
367
- ...base,
368
- ok: false,
369
- status: "ACTION_REQUIRED",
370
- data: setupPlan,
371
- actionRequired: {
372
- action: "reload-and-verify-extension",
373
- description: "Reload the unpacked extension, confirm its MV3 service worker is RUNNING, then rerun proflow-execution-browser-extension setup --extension-id <id>.",
374
- },
375
- },
376
- observedEffects: [],
377
- };
378
359
  },
379
360
  docs: async (_context) => ({
380
361
  result: {