@fieldwangai/agentflow 0.1.105 → 0.1.107

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.
@@ -152,11 +152,24 @@ import {
152
152
  addPrdWorkflowCollaborationMember,
153
153
  ensurePrdWorkflowCollaboration,
154
154
  getPrdWorkflowCollaborationById,
155
+ getPrdWorkflowCollaborationByShareToken,
155
156
  getPrdWorkflowCollaborationForUser,
157
+ ensurePrdWorkflowShareLink,
156
158
  prdWorkflowCollaborationAccess,
157
159
  prdWorkflowCollaborationSummary,
158
160
  removePrdWorkflowCollaborationMember,
161
+ revokePrdWorkflowShareLink,
159
162
  } from "./prd-workflow-collaboration.mjs";
163
+ import {
164
+ legacyOverallToGlobalState,
165
+ materializeWorkflowGlobalState,
166
+ mergeWorkflowArtifactLists,
167
+ mergeWorkflowArtifacts,
168
+ mergeWorkflowGlobalState,
169
+ normalizeWorkflowReference,
170
+ normalizeWorkflowReport,
171
+ workflowRuntimeRevision,
172
+ } from "./workflow-report.mjs";
160
173
 
161
174
  const MIME = {
162
175
  ".html": "text/html; charset=utf-8",
@@ -173,6 +186,11 @@ const MIME = {
173
186
  ".svg": "image/svg+xml",
174
187
  ".ico": "image/x-icon",
175
188
  };
189
+ const ADMIN_ONLY_USER_ENV_KEYS = new Set([
190
+ "CURSOR_API_KEYS",
191
+ "AGENTFLOW_CURSOR_API_KEY_COOLDOWN_MINUTES",
192
+ "CURSOR_API_KEY_COOLDOWN_MINUTES",
193
+ ]);
176
194
 
177
195
  const UI_SERVER_STARTED_AT = new Date().toISOString();
178
196
  const UI_SERVER_APP_VERSION = (() => {
@@ -3227,6 +3245,25 @@ function prdWorkflowCollaborationSummaryWithUsers(record, userId) {
3227
3245
  };
3228
3246
  }
3229
3247
 
3248
+ function prdWorkflowShareLinkSummary(record, shareToken, publicBaseUrl, userId = "") {
3249
+ const token = String(shareToken || record?.shareToken || "").trim();
3250
+ if (!record || !token) return null;
3251
+ const query = new URLSearchParams({
3252
+ view: "workflow",
3253
+ tapdId: String(record.tapdId || ""),
3254
+ workflowShare: token,
3255
+ });
3256
+ const base = String(publicBaseUrl || "").replace(/\/+$/, "");
3257
+ return {
3258
+ tapdId: String(record.tapdId || ""),
3259
+ url: `${base}/workspace?${query.toString()}`,
3260
+ active: true,
3261
+ readOnly: true,
3262
+ createdAt: record.shareCreatedAt || "",
3263
+ canManage: String(record.ownerId || "") === String(userId || "").trim().toLowerCase(),
3264
+ };
3265
+ }
3266
+
3230
3267
  function workspaceConversationsPath(scopedRoot) {
3231
3268
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "conversations.json");
3232
3269
  }
@@ -4065,9 +4102,50 @@ function workspaceSafeNodeOutputRelPath(value) {
4065
4102
  return clean;
4066
4103
  }
4067
4104
 
4068
- function workspaceDescribeNodeOutputsDir(nodeRunDir, maxEntries = 30) {
4069
- const outputsDir = path.resolve(nodeRunDir || "", "outputs");
4070
- if (!nodeRunDir || !fs.existsSync(outputsDir)) return "Current node outputs directory is missing.";
4105
+ function workspaceResolveOutputChild(rootDir, relativePath = "") {
4106
+ const root = path.resolve(rootDir || "");
4107
+ if (!rootDir || !root) return "";
4108
+ const parts = String(relativePath || "").split("/").filter(Boolean);
4109
+ const abs = path.resolve(root, ...parts);
4110
+ const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
4111
+ return abs === root || abs.startsWith(rootWithSep) ? abs : "";
4112
+ }
4113
+
4114
+ function workspaceNodeOutputSuffix(relPath) {
4115
+ const clean = workspaceSafeNodeOutputRelPath(relPath);
4116
+ return clean ? clean.slice("outputs/".length).replace(/^\/+/, "") : "";
4117
+ }
4118
+
4119
+ function workspaceNodeOutputWritePath(runPackage, relPath) {
4120
+ const clean = workspaceSafeNodeOutputRelPath(relPath);
4121
+ if (!clean) return "";
4122
+ const suffix = workspaceNodeOutputSuffix(clean);
4123
+ if (runPackage?.directWorkspaceOutputs && runPackage?.outputsDir) {
4124
+ return workspaceResolveOutputChild(runPackage.outputsDir, suffix);
4125
+ }
4126
+ return workspaceResolveOutputChild(runPackage?.nodeRunDir, clean);
4127
+ }
4128
+
4129
+ function workspaceNodeOutputCandidates(runPackage, relPath) {
4130
+ const clean = workspaceSafeNodeOutputRelPath(relPath);
4131
+ if (!clean) return [];
4132
+ const suffix = workspaceNodeOutputSuffix(clean);
4133
+ const direct = runPackage?.outputsDir
4134
+ ? workspaceResolveOutputChild(runPackage.outputsDir, suffix)
4135
+ : "";
4136
+ const legacy = runPackage?.nodeRunDir
4137
+ ? workspaceResolveOutputChild(runPackage.nodeRunDir, clean)
4138
+ : "";
4139
+ const ordered = runPackage?.directWorkspaceOutputs ? [direct, legacy] : [legacy, direct];
4140
+ return ordered.filter((candidate, index, list) => candidate && list.indexOf(candidate) === index);
4141
+ }
4142
+
4143
+ function workspaceDescribeNodeOutputsDir(runPackage, maxEntries = 30) {
4144
+ const outputDirs = [
4145
+ runPackage?.outputsDir,
4146
+ runPackage?.nodeRunDir ? path.resolve(runPackage.nodeRunDir, "outputs") : "",
4147
+ ].filter((dir, index, list) => dir && list.indexOf(dir) === index && fs.existsSync(dir));
4148
+ if (!outputDirs.length) return "Current node outputs directory is missing.";
4071
4149
  const entries = [];
4072
4150
  const walk = (dir, rel = "") => {
4073
4151
  if (entries.length >= maxEntries) return;
@@ -4084,7 +4162,7 @@ function workspaceDescribeNodeOutputsDir(nodeRunDir, maxEntries = 30) {
4084
4162
  if (child.isDirectory()) walk(path.join(dir, child.name), childRel);
4085
4163
  }
4086
4164
  };
4087
- walk(outputsDir);
4165
+ for (const outputDir of outputDirs) walk(outputDir);
4088
4166
  if (!entries.length) return "Current node outputs directory is empty.";
4089
4167
  const suffix = entries.length >= maxEntries ? `\n...showing first ${maxEntries} entries` : "";
4090
4168
  return `Current node outputs entries:\n${entries.map((entry) => `- outputs/${entry}`).join("\n")}${suffix}`;
@@ -4097,29 +4175,31 @@ function workspacePublishNodeOutputFile(runPackage, relPath) {
4097
4175
  const nodeRunDir = path.resolve(runPackage?.nodeRunDir || "");
4098
4176
  const workspaceOutputsDir = path.resolve(runPackage?.workspaceOutputsDir || "");
4099
4177
  if (!nodeRunDir || !workspaceOutputsDir) return clean;
4100
- const src = path.resolve(nodeRunDir, clean);
4101
- const nodeRootWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
4102
- if (src !== nodeRunDir && !src.startsWith(nodeRootWithSep)) {
4103
- throw new Error(`Invalid node output path: ${clean}`);
4104
- }
4105
- if (!fs.existsSync(src) || !fs.statSync(src).isFile()) {
4178
+ const src = workspaceNodeOutputCandidates(runPackage, clean)
4179
+ .find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
4180
+ if (!src) {
4106
4181
  throw new Error(
4107
4182
  `Agent returned resultFile but did not create it under this node's outputs: ${clean}\n` +
4108
- `Expected file: ${src}\n` +
4109
- `${workspaceDescribeNodeOutputsDir(nodeRunDir)}\n` +
4110
- `Write outputs using a relative path from the node cwd, or use AGENTFLOW_OUTPUTS_DIR.`
4183
+ `Expected file: ${workspaceNodeOutputWritePath(runPackage, clean)}\n` +
4184
+ `${workspaceDescribeNodeOutputsDir(runPackage)}\n` +
4185
+ `Write downloadable files to the absolute AGENTFLOW_OUTPUTS_DIR path.`
4111
4186
  );
4112
4187
  }
4113
4188
  const nodePart = workspaceSanitizeTmpSegment(runPackage?.nodeId || "node", "node");
4114
- const destRel = clean.slice("outputs/".length).replace(/^\/+/, "");
4115
- const publishedRel = path.posix.join("outputs", nodePart, ...destRel.split("/").filter(Boolean));
4116
- const dest = path.resolve(workspaceOutputsDir, nodePart, ...destRel.split("/").filter(Boolean));
4189
+ const destRel = workspaceNodeOutputSuffix(clean);
4190
+ const publishedBase = String(runPackage?.outputsRel || "").trim() || path.posix.join("outputs", nodePart);
4191
+ const publishedRel = path.posix.join(publishedBase, ...destRel.split("/").filter(Boolean));
4192
+ const dest = runPackage?.directWorkspaceOutputs && runPackage?.outputsDir
4193
+ ? workspaceResolveOutputChild(runPackage.outputsDir, destRel)
4194
+ : path.resolve(workspaceOutputsDir, nodePart, ...destRel.split("/").filter(Boolean));
4117
4195
  const workspaceOutputsWithSep = workspaceOutputsDir.endsWith(path.sep) ? workspaceOutputsDir : `${workspaceOutputsDir}${path.sep}`;
4118
4196
  if (dest !== workspaceOutputsDir && !dest.startsWith(workspaceOutputsWithSep)) {
4119
4197
  throw new Error(`Invalid workspace output path: ${clean}`);
4120
4198
  }
4121
- fs.mkdirSync(path.dirname(dest), { recursive: true });
4122
- fs.copyFileSync(src, dest);
4199
+ if (src !== dest) {
4200
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
4201
+ fs.copyFileSync(src, dest);
4202
+ }
4123
4203
  return publishedRel;
4124
4204
  }
4125
4205
 
@@ -4130,16 +4210,15 @@ export function workspaceMaterializeAgentResultFile(structured, runPackage) {
4130
4210
  const declared = workspaceSafeNodeOutputRelPath(rawDeclared);
4131
4211
  if (rawDeclared && !declared) return structured;
4132
4212
  const relPath = declared || configured;
4133
- const abs = path.resolve(runPackage.nodeRunDir || "", relPath);
4134
- const nodeRunDir = path.resolve(runPackage.nodeRunDir || "");
4135
- const nodeRootWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
4136
- if (!nodeRunDir || (abs !== nodeRunDir && !abs.startsWith(nodeRootWithSep))) return structured;
4213
+ const abs = workspaceNodeOutputWritePath(runPackage, relPath);
4214
+ if (!abs) return structured;
4137
4215
 
4138
4216
  const explicitInlineResult = structured.parsed && typeof structured.parsed === "object"
4139
4217
  ? String(structured.parsed.result ?? "")
4140
4218
  : "";
4141
4219
  const content = declared ? explicitInlineResult : String(structured.result ?? "");
4142
- let primaryReady = fs.existsSync(abs) && fs.statSync(abs).isFile();
4220
+ let primaryReady = workspaceNodeOutputCandidates(runPackage, relPath)
4221
+ .some((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
4143
4222
  if (!primaryReady && content.trim()) {
4144
4223
  fs.mkdirSync(path.dirname(abs), { recursive: true });
4145
4224
  const tmp = path.join(path.dirname(abs), `.${path.basename(abs)}.${process.pid}.${Date.now()}.tmp`);
@@ -4164,8 +4243,8 @@ export function workspaceMaterializeAgentResultFile(structured, runPackage) {
4164
4243
  if (!outputContent.trim()) continue;
4165
4244
  const outputRel = workspaceSafeNodeOutputRelPath(configuredRel);
4166
4245
  if (!outputRel) continue;
4167
- const outputAbs = path.resolve(nodeRunDir, outputRel);
4168
- if (outputAbs !== nodeRunDir && !outputAbs.startsWith(nodeRootWithSep)) continue;
4246
+ const outputAbs = workspaceNodeOutputWritePath(runPackage, outputRel);
4247
+ if (!outputAbs) continue;
4169
4248
  fs.mkdirSync(path.dirname(outputAbs), { recursive: true });
4170
4249
  fs.writeFileSync(outputAbs, outputContent, "utf-8");
4171
4250
  delete outParams[name];
@@ -4182,18 +4261,73 @@ export function workspaceMaterializeAgentResultFile(structured, runPackage) {
4182
4261
  };
4183
4262
  }
4184
4263
 
4264
+ function workspaceCollectNodeOutputFiles(runPackage, maxFiles = 500) {
4265
+ const roots = [
4266
+ runPackage?.outputsDir,
4267
+ runPackage?.nodeRunDir ? path.resolve(runPackage.nodeRunDir, "outputs") : "",
4268
+ ].filter((dir, index, list) => dir && list.indexOf(dir) === index && fs.existsSync(dir));
4269
+ const relativeFiles = new Set();
4270
+ const walk = (root, dir, depth = 0) => {
4271
+ if (depth > 12 || relativeFiles.size >= maxFiles) return;
4272
+ let entries = [];
4273
+ try {
4274
+ entries = fs.readdirSync(dir, { withFileTypes: true });
4275
+ } catch {
4276
+ return;
4277
+ }
4278
+ for (const entry of entries) {
4279
+ if (relativeFiles.size >= maxFiles) break;
4280
+ if (entry.isSymbolicLink()) continue;
4281
+ const abs = path.join(dir, entry.name);
4282
+ if (entry.isDirectory()) {
4283
+ walk(root, abs, depth + 1);
4284
+ } else if (entry.isFile()) {
4285
+ relativeFiles.add(path.relative(root, abs).replace(/\\/g, "/"));
4286
+ }
4287
+ }
4288
+ };
4289
+ for (const root of roots) walk(root, root);
4290
+
4291
+ const outputFiles = [];
4292
+ for (const relativePath of relativeFiles) {
4293
+ const publishedPath = workspacePublishNodeOutputFile(runPackage, `outputs/${relativePath}`);
4294
+ if (!publishedPath) continue;
4295
+ const publishedAbs = workspaceResolveOutputChild(
4296
+ runPackage.workspaceOutputsDir,
4297
+ publishedPath.slice("outputs/".length),
4298
+ );
4299
+ let size = 0;
4300
+ try {
4301
+ size = publishedAbs ? fs.statSync(publishedAbs).size : 0;
4302
+ } catch {}
4303
+ outputFiles.push({ path: publishedPath, name: path.posix.basename(publishedPath), size });
4304
+ }
4305
+ outputFiles.sort((a, b) => a.path.localeCompare(b.path));
4306
+ return outputFiles;
4307
+ }
4308
+
4185
4309
  export function workspacePublishAgentOutputFiles(structured, runPackage) {
4186
- if (!structured?.structured || !runPackage) return structured;
4187
- const resultFile = workspacePublishNodeOutputFile(runPackage, structured.resultFile);
4188
- const outParams = { ...(structured.outParams || {}) };
4310
+ if (!runPackage) return structured;
4311
+ const base = structured && typeof structured === "object" ? structured : {};
4312
+ const resultFile = base.structured
4313
+ ? workspacePublishNodeOutputFile(runPackage, base.resultFile)
4314
+ : "";
4315
+ const outParams = { ...(base.outParams || {}) };
4189
4316
  for (const [key, value] of Object.entries(outParams)) {
4190
4317
  if (!String(key || "").endsWith("File")) continue;
4191
4318
  const published = workspacePublishNodeOutputFile(runPackage, value);
4192
4319
  if (published) outParams[key] = published;
4193
4320
  }
4194
- return resultFile || Object.keys(outParams).length
4195
- ? { ...structured, result: resultFile || structured.result, resultFile: resultFile || structured.resultFile, outParams }
4196
- : structured;
4321
+ const outputFiles = workspaceCollectNodeOutputFiles(runPackage);
4322
+ return resultFile || Object.keys(outParams).length || outputFiles.length
4323
+ ? {
4324
+ ...base,
4325
+ result: resultFile || base.result,
4326
+ resultFile: resultFile || base.resultFile,
4327
+ outParams,
4328
+ outputFiles,
4329
+ }
4330
+ : base;
4197
4331
  }
4198
4332
 
4199
4333
  function workspaceOutputFieldForSlot(slot, index = 0) {
@@ -4679,6 +4813,7 @@ function workspaceAgentInputBlock(inputValues = {}, inputMounts = {}) {
4679
4813
  function workspaceNodeFileBoundaryBlock(runPackage = {}) {
4680
4814
  const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
4681
4815
  const nodeTmpDir = String(runPackage?.nodeTmpDir || "").trim();
4816
+ const outputsDir = String(runPackage?.outputsDir || "").trim();
4682
4817
  const outputsRel = String(runPackage?.outputsRel || "outputs").trim() || "outputs";
4683
4818
  if (!nodeRunDir && !nodeTmpDir) return "";
4684
4819
  return [
@@ -4688,7 +4823,9 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
4688
4823
  nodeTmpDir ? `- 临时文件只能写入:\`${nodeTmpDir}\`,也可通过环境变量 \`AGENTFLOW_NODE_TMP_DIR\` 获取。` : "",
4689
4824
  Object.keys(runPackage?.inputMounts || {}).length ? "- 已挂载的输入文件位于本任务 `inputs/`;`inputs/` 只用于读取,正式产物仍写入 `outputs/`。" : "",
4690
4825
  "- 主文本结果和内联额外输出由 AgentFlow 在任务结束后自动写入、发布和清理,无需自行创建结果文件。",
4691
- `- 任务若必须直接生成二进制或工程文件,只能写入本任务 \`${outputsRel}/\`;可使用 \`AGENTFLOW_OUTPUTS_DIR\` 获取绝对目录。`,
4826
+ outputsDir ? `- 可供用户下载的最终产物目录:\`${outputsDir}\`。这不是临时目录,环境变量 \`AGENTFLOW_OUTPUTS_DIR\` 指向这里。` : "",
4827
+ outputsDir ? `- CSV、图片、压缩包、工程文件等下载产物必须直接写入 \`AGENTFLOW_OUTPUTS_DIR\`,不要写入当前执行目录中的相对 \`outputs/\`。` : "",
4828
+ outputsDir ? `- 该目录中的文件会自动出现在 Workspace Files,对应相对路径为 \`${outputsRel}/\`;回复中引用下载文件时使用这个相对路径。` : "",
4692
4829
  "- 不要在执行目录根部创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
4693
4830
  "- 不要自行删除 run package;AgentFlow 会在运行结束后统一清理。",
4694
4831
  ].filter(Boolean).join("\n");
@@ -5953,14 +6090,20 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
5953
6090
  return dir;
5954
6091
  }
5955
6092
 
5956
- function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "", task = "", inputValues = {}, skillsBlock = "", mcpBlock = "", resultFile = "", outParamFiles = {} } = {}) {
6093
+ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "", task = "", inputValues = {}, skillsBlock = "", mcpBlock = "", resultFile = "", outParamFiles = {}, durableOutputs = false } = {}) {
5957
6094
  const nodeRunDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
5958
6095
  const nodeTmpDir = path.join(nodeRunDir, "tmp");
5959
- const outputsDir = path.join(nodeRunDir, "outputs");
6096
+ const legacyOutputsDir = path.join(nodeRunDir, "outputs");
5960
6097
  const workspaceRoot = path.resolve(scopedRoot);
5961
6098
  const workspaceOutputsDir = path.join(workspaceRoot, "outputs");
6099
+ const nodePart = workspaceSanitizeTmpSegment(nodeId || "node", "node");
6100
+ const outputsRel = durableOutputs ? path.posix.join("outputs", nodePart) : "outputs";
6101
+ const outputsDir = durableOutputs ? path.join(workspaceOutputsDir, nodePart) : legacyOutputsDir;
5962
6102
  const resultFileRel = workspaceSafeNodeOutputRelPath(resultFile) || "";
5963
- const resultFileAbs = resultFileRel ? path.resolve(nodeRunDir, resultFileRel) : "";
6103
+ const resultFileSuffix = workspaceNodeOutputSuffix(resultFileRel);
6104
+ const resultFileAbs = resultFileRel
6105
+ ? workspaceResolveOutputChild(outputsDir, resultFileSuffix)
6106
+ : "";
5964
6107
  const safeOutParamFiles = {};
5965
6108
  for (const [name, rel] of Object.entries(outParamFiles || {})) {
5966
6109
  const cleanName = String(name || "").trim();
@@ -5968,6 +6111,8 @@ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "
5968
6111
  if (cleanName && cleanRel) safeOutParamFiles[cleanName] = cleanRel;
5969
6112
  }
5970
6113
  fs.mkdirSync(nodeTmpDir, { recursive: true });
6114
+ fs.mkdirSync(legacyOutputsDir, { recursive: true });
6115
+ if (durableOutputs) fs.rmSync(outputsDir, { recursive: true, force: true });
5971
6116
  fs.mkdirSync(outputsDir, { recursive: true });
5972
6117
  fs.mkdirSync(workspaceOutputsDir, { recursive: true });
5973
6118
  const manifest = {
@@ -5976,7 +6121,11 @@ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "
5976
6121
  nodeRunDir,
5977
6122
  nodeTmpDir,
5978
6123
  outputsDir,
6124
+ legacyOutputsDir,
6125
+ outputsRel,
6126
+ directWorkspaceOutputs: durableOutputs,
5979
6127
  workspaceRoot,
6128
+ workspaceOutputsDir,
5980
6129
  executionCwd: cwd ? path.resolve(cwd) : workspaceRoot,
5981
6130
  resultFileRel,
5982
6131
  resultFileAbs,
@@ -6001,8 +6150,6 @@ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "
6001
6150
  }
6002
6151
  return {
6003
6152
  ...manifest,
6004
- workspaceOutputsDir,
6005
- outputsRel: "outputs",
6006
6153
  inputValues: runtimeInputValues,
6007
6154
  inputMounts: materializedInputs.mounts,
6008
6155
  };
@@ -6840,6 +6987,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6840
6987
  cwd,
6841
6988
  task: String(instance.script || instance.scriptRef || instance.body || "").trim(),
6842
6989
  inputValues,
6990
+ durableOutputs: true,
6843
6991
  });
6844
6992
  const runtimeInputValues = { ...inputValues, ...(runPackage.inputValues || {}) };
6845
6993
  emitTiming(nodeId, "prepare-script", prepareStartedAt, {
@@ -6876,7 +7024,12 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6876
7024
  if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
6877
7025
  const updatedDisplays = propagateNodeOutputDisplays(nodeId, resultContent);
6878
7026
  if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
6879
- emit({ type: "node-done", nodeId, definitionId: defId });
7027
+ emit({
7028
+ type: "node-done",
7029
+ nodeId,
7030
+ definitionId: defId,
7031
+ outputFiles: normalizedAgentOutput.outputFiles || [],
7032
+ });
6880
7033
  continue;
6881
7034
  }
6882
7035
 
@@ -6900,6 +7053,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6900
7053
  mcpBlock: promptMcpBlock,
6901
7054
  resultFile: resultOutputSpec.resultFile,
6902
7055
  outParamFiles: workspaceOutParamFileSpecs(graph, nodeId),
7056
+ durableOutputs: true,
6903
7057
  });
6904
7058
  const runtimeInputValues = { ...inputValues, ...(runPackage.inputValues || {}) };
6905
7059
  const body = workspaceResolveBodyPlaceholders(instance.body || "", runtimeInputValues).trim();
@@ -6950,6 +7104,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6950
7104
  const handle = startComposerAgent({
6951
7105
  uiWorkspaceRoot: scopedRoot,
6952
7106
  cliWorkspace: runPackage.nodeRunDir,
7107
+ writableDirs: [runPackage.outputsDir],
6953
7108
  prompt,
6954
7109
  modelKey: nodeModelKey,
6955
7110
  agentflowUserId: userCtx.userId || "",
@@ -7039,7 +7194,12 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
7039
7194
  }
7040
7195
  const updatedDisplays = propagateNodeOutputDisplays(nodeId, resultContent);
7041
7196
  if (slotUpdate.changed || implementationUpdate.changed || contextRunOutputChanged || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
7042
- emit({ type: "node-done", nodeId, definitionId: defId });
7197
+ emit({
7198
+ type: "node-done",
7199
+ nodeId,
7200
+ definitionId: defId,
7201
+ outputFiles: normalizedAgentOutput.outputFiles || [],
7202
+ });
7043
7203
  }
7044
7204
  } finally {
7045
7205
  workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
@@ -7253,14 +7413,22 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7253
7413
  const flowId = String(params.flowId || "").trim();
7254
7414
  const flowSource = String(params.flowSource || "user").trim() || "user";
7255
7415
  const archived = params.archived === true || params.archived === "1" || params.flowArchived === true;
7256
- const collaboration = tapdId
7416
+ const shareToken = String(params.workflowShare || params.workflow_share || "").trim();
7417
+ const linkCollaboration = shareToken ? getPrdWorkflowCollaborationByShareToken(shareToken) : null;
7418
+ if (shareToken && (!linkCollaboration || linkCollaboration.tapdId !== tapdId)) {
7419
+ return { error: "Workflow share link is invalid or has been revoked", status: 404 };
7420
+ }
7421
+ const memberCollaboration = tapdId
7257
7422
  ? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
7258
7423
  : null;
7259
- const access = prdWorkflowCollaborationAccess(collaboration, userCtx?.userId);
7424
+ const collaboration = linkCollaboration || memberCollaboration;
7425
+ const access = linkCollaboration
7426
+ ? { allowed: true, writable: false, role: "viewer", via: "share-link" }
7427
+ : prdWorkflowCollaborationAccess(collaboration, userCtx?.userId);
7260
7428
  if (collaboration && !access.allowed) {
7261
7429
  return { error: "PRD Workflow collaboration permission denied", status: 403 };
7262
7430
  }
7263
- if (capability === "write" && collaboration && !access.writable) {
7431
+ if (capability === "write" && (linkCollaboration || (collaboration && !access.writable))) {
7264
7432
  return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
7265
7433
  }
7266
7434
  const ownerId = String(collaboration?.ownerId || userCtx?.userId || "").trim();
@@ -7287,15 +7455,18 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7287
7455
  ownerId,
7288
7456
  collaboration,
7289
7457
  collaborationAccess: access,
7458
+ shareToken,
7459
+ sharedByLink: Boolean(linkCollaboration),
7290
7460
  flowId,
7291
7461
  flowSource,
7292
7462
  archived,
7293
7463
  };
7294
7464
  }
7295
7465
 
7296
- function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId = "") {
7466
+ function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId = "", shareToken = "") {
7297
7467
  const id = String(tapdId || "").trim();
7298
- const collaboration = getPrdWorkflowCollaborationForUser(id, userCtx?.userId);
7468
+ const collaboration = getPrdWorkflowCollaborationByShareToken(shareToken)
7469
+ || getPrdWorkflowCollaborationForUser(id, userCtx?.userId);
7299
7470
  const actorScope = `user:${String(collaboration?.ownerId || userCtx?.userId || "")}`;
7300
7471
  return [actorScope, id].join("\t");
7301
7472
  }
@@ -8800,6 +8971,19 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
8800
8971
  incomingImplementationMetadata,
8801
8972
  )
8802
8973
  : previousImplementationMetadata;
8974
+ const previousGlobalStatePatch =
8975
+ events[index]?.globalStatePatch || events[index]?.global_state_patch;
8976
+ const incomingGlobalStatePatch =
8977
+ entry?.globalStatePatch || entry?.global_state_patch;
8978
+ const mergedGlobalStatePatch =
8979
+ incomingGlobalStatePatch && typeof incomingGlobalStatePatch === "object" && !Array.isArray(incomingGlobalStatePatch)
8980
+ ? mergeWorkflowGlobalState(
8981
+ previousGlobalStatePatch && typeof previousGlobalStatePatch === "object" && !Array.isArray(previousGlobalStatePatch)
8982
+ ? previousGlobalStatePatch
8983
+ : {},
8984
+ incomingGlobalStatePatch,
8985
+ )
8986
+ : previousGlobalStatePatch;
8803
8987
  const prevArtifact = prdWorkflowRuntimeEventArtifactSignature(events[index]);
8804
8988
  const nextArtifact = prdWorkflowRuntimeEventArtifactSignature(entry);
8805
8989
  artifactConflict = Boolean(prevArtifact && nextArtifact && prevArtifact !== nextArtifact &&
@@ -8815,9 +8999,14 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
8815
8999
  ...events[index],
8816
9000
  ...entry,
8817
9001
  links: prdWorkflowMergeRuntimeEventArrays(events[index].links, entry.links),
8818
- artifacts: prdWorkflowMergeRuntimeEventArrays(events[index].artifacts, entry.artifacts),
9002
+ artifacts: mergeWorkflowArtifactLists(events[index].artifacts, entry.artifacts, entry.artifactScope || "action"),
8819
9003
  outputs: prdWorkflowMergeRuntimeEventArrays(events[index].outputs, entry.outputs),
8820
9004
  results: prdWorkflowMergeRuntimeEventArrays(events[index].results, entry.results),
9005
+ actionModel: mergeWorkflowGlobalState(events[index].actionModel, entry.actionModel),
9006
+ globalStateRemove: prdWorkflowMergeRuntimeEventArrays(
9007
+ events[index].globalStateRemove || events[index].global_state_remove,
9008
+ entry.globalStateRemove || entry.global_state_remove,
9009
+ ),
8821
9010
  createdAt: events[index].createdAt || entry.createdAt,
8822
9011
  startedAt: events[index].startedAt || entry.startedAt,
8823
9012
  idempotencyHistory: [...new Set(idempotencyHistory)].slice(-50),
@@ -8825,6 +9014,9 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
8825
9014
  if (mergedImplementationMetadata && typeof mergedImplementationMetadata === "object" && !Array.isArray(mergedImplementationMetadata)) {
8826
9015
  events[index].implementationMetadata = mergedImplementationMetadata;
8827
9016
  }
9017
+ if (mergedGlobalStatePatch && typeof mergedGlobalStatePatch === "object" && !Array.isArray(mergedGlobalStatePatch)) {
9018
+ events[index].globalStatePatch = mergedGlobalStatePatch;
9019
+ }
8828
9020
  if (artifactConflict) {
8829
9021
  events[index] = {
8830
9022
  ...events[index],
@@ -8915,7 +9107,7 @@ function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = [
8915
9107
  ...out[index],
8916
9108
  ...event,
8917
9109
  links: prdWorkflowMergeRuntimeEventArrays(out[index].links, event.links),
8918
- artifacts: prdWorkflowMergeRuntimeEventArrays(out[index].artifacts, event.artifacts),
9110
+ artifacts: mergeWorkflowArtifactLists(out[index].artifacts, event.artifacts, event.artifactScope || "action"),
8919
9111
  outputs: prdWorkflowMergeRuntimeEventArrays(out[index].outputs, event.outputs),
8920
9112
  results: prdWorkflowMergeRuntimeEventArrays(out[index].results, event.results),
8921
9113
  };
@@ -9123,13 +9315,55 @@ function prdWorkflowOverallFromEvents(tapdId, snapshot = {}, runtimeEvents = [])
9123
9315
  return prdWorkflowFinalizeOverall(tapdId, overall);
9124
9316
  }
9125
9317
 
9318
+ function prdWorkflowEventUpdatesOverall(event) {
9319
+ if (!event || typeof event !== "object" || Array.isArray(event)) return false;
9320
+ return Boolean(
9321
+ event.overallPatch ||
9322
+ event.overall_patch ||
9323
+ event.overallOwnerFromActor === true ||
9324
+ event.overall_owner_from_actor === true ||
9325
+ event.implementationMetadata ||
9326
+ event.implementation_metadata ||
9327
+ (Array.isArray(event.overallRemove) && event.overallRemove.length) ||
9328
+ (Array.isArray(event.overall_remove) && event.overall_remove.length)
9329
+ );
9330
+ }
9331
+
9332
+ function prdWorkflowGlobalStateFromEvents(tapdId, snapshot = {}, runtimeEvents = []) {
9333
+ let legacyOverall = prdWorkflowOverallFromEvents(tapdId, snapshot, []);
9334
+ let state = materializeWorkflowGlobalState(tapdId, snapshot, [], legacyOverall);
9335
+ const events = [...(Array.isArray(runtimeEvents) ? runtimeEvents : [])].sort((left, right) => {
9336
+ const leftAt = Date.parse(left?.updatedAt || left?.occurredAt || left?.completedAt || left?.createdAt || left?.observedAt || "");
9337
+ const rightAt = Date.parse(right?.updatedAt || right?.occurredAt || right?.completedAt || right?.createdAt || right?.observedAt || "");
9338
+ if (!Number.isFinite(leftAt) && !Number.isFinite(rightAt)) return 0;
9339
+ if (!Number.isFinite(leftAt)) return -1;
9340
+ if (!Number.isFinite(rightAt)) return 1;
9341
+ return leftAt - rightAt;
9342
+ });
9343
+ for (const event of events) {
9344
+ if (prdWorkflowEventUpdatesOverall(event)) {
9345
+ legacyOverall = prdWorkflowOverallFromEvents(tapdId, { overall: legacyOverall }, [event]);
9346
+ state = mergeWorkflowGlobalState(state, legacyOverallToGlobalState(tapdId, legacyOverall));
9347
+ }
9348
+ state = materializeWorkflowGlobalState(tapdId, { globalState: state }, [event], {});
9349
+ }
9350
+ return state;
9351
+ }
9352
+
9126
9353
  function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
9127
9354
  const runtime = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
9128
9355
  const runtimeEvents = runtime.events;
9129
9356
  const events = prdWorkflowMergeRuntimeEventList(snapshot?.events, runtimeEvents);
9357
+ const overall = prdWorkflowOverallFromEvents(tapdId, snapshot, runtimeEvents);
9358
+ const globalState = prdWorkflowGlobalStateFromEvents(tapdId, snapshot, runtimeEvents);
9359
+ const artifacts = mergeWorkflowArtifacts(snapshot?.artifacts, runtimeEvents);
9130
9360
  return {
9131
9361
  ...snapshot,
9132
- overall: prdWorkflowOverallFromEvents(tapdId, snapshot, runtimeEvents),
9362
+ workflow: globalState.workflow,
9363
+ overall,
9364
+ globalState,
9365
+ artifacts,
9366
+ runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents),
9133
9367
  runtimeEvents,
9134
9368
  events,
9135
9369
  sources: {
@@ -10500,6 +10734,116 @@ export function startUiServer({
10500
10734
  json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
10501
10735
  return;
10502
10736
  }
10737
+ if (req.method === "GET" && url.pathname === "/api/prd-workflow/share") {
10738
+ const tapdId = String(url.searchParams.get("tapdId") || "").trim();
10739
+ const shareToken = String(url.searchParams.get("workflowShare") || "").trim();
10740
+ if (shareToken) {
10741
+ const record = getPrdWorkflowCollaborationByShareToken(shareToken);
10742
+ if (!record || (tapdId && record.tapdId !== tapdId)) {
10743
+ json(res, 404, { error: "Workflow share link is invalid or has been revoked" });
10744
+ return;
10745
+ }
10746
+ json(res, 200, {
10747
+ ok: true,
10748
+ share: prdWorkflowShareLinkSummary(
10749
+ record,
10750
+ shareToken,
10751
+ serverPublicBaseUrl(req, host, uiPort),
10752
+ userCtx.userId,
10753
+ ),
10754
+ });
10755
+ return;
10756
+ }
10757
+ if (!authUser?.userId) {
10758
+ json(res, 401, { error: "Unauthorized" });
10759
+ return;
10760
+ }
10761
+ if (!tapdId) {
10762
+ json(res, 400, { error: "Missing tapdId" });
10763
+ return;
10764
+ }
10765
+ const record = getPrdWorkflowCollaborationForUser(tapdId, userCtx.userId);
10766
+ const role = prdWorkflowCollaborationAccess(record, userCtx.userId).role;
10767
+ json(res, 200, {
10768
+ ok: true,
10769
+ canCreate: !record || role === "owner",
10770
+ share: record?.shareToken
10771
+ ? prdWorkflowShareLinkSummary(
10772
+ record,
10773
+ record.shareToken,
10774
+ serverPublicBaseUrl(req, host, uiPort),
10775
+ userCtx.userId,
10776
+ )
10777
+ : null,
10778
+ });
10779
+ return;
10780
+ }
10781
+ if (req.method === "POST" && url.pathname === "/api/prd-workflow/share") {
10782
+ let payload;
10783
+ try {
10784
+ payload = JSON.parse(await readBody(req));
10785
+ } catch {
10786
+ json(res, 400, { error: "Invalid JSON body" });
10787
+ return;
10788
+ }
10789
+ if (!authUser?.userId) {
10790
+ json(res, 401, { error: "Unauthorized" });
10791
+ return;
10792
+ }
10793
+ const tapdId = String(payload?.tapdId || payload?.tapd_id || "").trim();
10794
+ if (!tapdId) {
10795
+ json(res, 400, { error: "Missing tapdId" });
10796
+ return;
10797
+ }
10798
+ const existing = getPrdWorkflowCollaborationForUser(tapdId, userCtx.userId);
10799
+ if (existing && prdWorkflowCollaborationAccess(existing, userCtx.userId).role !== "owner") {
10800
+ json(res, 403, { error: "仅 Workflow 所有者可以创建分享链接" });
10801
+ return;
10802
+ }
10803
+ const result = ensurePrdWorkflowShareLink({ tapdId, userId: userCtx.userId });
10804
+ if (result.error) {
10805
+ json(res, result.status || 400, { error: result.error });
10806
+ return;
10807
+ }
10808
+ const scope = resolvePrdWorkflowScope(root, { ...payload, tapdId }, userCtx, "write");
10809
+ if (!scope.error) prdWorkflowMigrateLegacyState(scope.executionRoot, scope.stateRoot, tapdId);
10810
+ json(res, 200, {
10811
+ ok: true,
10812
+ created: result.created === true,
10813
+ share: prdWorkflowShareLinkSummary(
10814
+ result.record,
10815
+ result.shareToken,
10816
+ serverPublicBaseUrl(req, host, uiPort, payload),
10817
+ userCtx.userId,
10818
+ ),
10819
+ });
10820
+ return;
10821
+ }
10822
+ if (req.method === "DELETE" && url.pathname === "/api/prd-workflow/share") {
10823
+ let payload;
10824
+ try {
10825
+ payload = JSON.parse(await readBody(req));
10826
+ } catch {
10827
+ json(res, 400, { error: "Invalid JSON body" });
10828
+ return;
10829
+ }
10830
+ if (!authUser?.userId) {
10831
+ json(res, 401, { error: "Unauthorized" });
10832
+ return;
10833
+ }
10834
+ const tapdId = String(payload?.tapdId || payload?.tapd_id || "").trim();
10835
+ if (!tapdId) {
10836
+ json(res, 400, { error: "Missing tapdId" });
10837
+ return;
10838
+ }
10839
+ const result = revokePrdWorkflowShareLink({ tapdId, userId: userCtx.userId });
10840
+ if (result.error) {
10841
+ json(res, result.status || 400, { error: result.error });
10842
+ return;
10843
+ }
10844
+ json(res, 200, { ok: true, revoked: result.revoked === true, share: null });
10845
+ return;
10846
+ }
10503
10847
  if (req.method === "GET" && url.pathname === "/api/prd-workflow/collaboration") {
10504
10848
  const tapdId = String(url.searchParams.get("tapdId") || "").trim();
10505
10849
  if (!tapdId) {
@@ -10611,6 +10955,55 @@ export function startUiServer({
10611
10955
  }
10612
10956
  return;
10613
10957
  }
10958
+ if (req.method === "GET" && url.pathname === "/api/workflows/state") {
10959
+ try {
10960
+ const workflow = normalizeWorkflowReference({
10961
+ workflow: {
10962
+ key: url.searchParams.get("workflow") || "",
10963
+ namespace: url.searchParams.get("namespace") || "",
10964
+ id: url.searchParams.get("id") || "",
10965
+ },
10966
+ });
10967
+ if (workflow.error) {
10968
+ json(res, 400, { error: workflow.error });
10969
+ return;
10970
+ }
10971
+ if (workflow.namespace !== "tapd") {
10972
+ json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
10973
+ return;
10974
+ }
10975
+ const flowId = String(url.searchParams.get("flowId") || "").trim();
10976
+ const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
10977
+ const workflowScope = resolvePrdWorkflowScope(root, {
10978
+ tapdId: workflow.id,
10979
+ flowId,
10980
+ flowSource,
10981
+ archived: url.searchParams.get("archived") === "1",
10982
+ workspaceId: url.searchParams.get("workspaceId") || "",
10983
+ workflowShare: url.searchParams.get("workflowShare") || "",
10984
+ }, userCtx);
10985
+ if (workflowScope.error) {
10986
+ json(res, workflowScope.status || 400, { error: workflowScope.error });
10987
+ return;
10988
+ }
10989
+ const scopedRoot = workflowScope.stateRoot;
10990
+ prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, workflow.id);
10991
+ const runtimeOnly = url.searchParams.get("runtimeOnly") === "1" ||
10992
+ url.searchParams.get("runtime_only") === "1" ||
10993
+ url.searchParams.get("cached") === "1";
10994
+ const baseSnapshot = runtimeOnly
10995
+ ? prdWorkflowMaterializeSnapshot(workflowScope.executionRoot, scopedRoot, workflow.id, userCtx, { flowSource, flowId })
10996
+ : await prdWorkflowSnapshot(workflowScope.executionRoot, scopedRoot, workflow.id, userCtx, { flowSource, flowId });
10997
+ const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
10998
+ baseSnapshot,
10999
+ getSessionTokenFromRequest(req) || "",
11000
+ );
11001
+ json(res, 200, { ok: true, workflow, snapshot });
11002
+ } catch (e) {
11003
+ json(res, 500, { error: (e && e.message) || String(e) });
11004
+ }
11005
+ return;
11006
+ }
10614
11007
  if (req.method === "GET" && url.pathname === "/api/prd-workflow/snapshot") {
10615
11008
  try {
10616
11009
  const tapdId = String(url.searchParams.get("tapdId") || "").trim();
@@ -10623,6 +11016,7 @@ export function startUiServer({
10623
11016
  flowSource,
10624
11017
  archived,
10625
11018
  workspaceId: url.searchParams.get("workspaceId") || "",
11019
+ workflowShare: url.searchParams.get("workflowShare") || "",
10626
11020
  }, userCtx);
10627
11021
  if (workflowScope.error) {
10628
11022
  json(res, workflowScope.status || 400, { error: workflowScope.error });
@@ -10651,6 +11045,10 @@ export function startUiServer({
10651
11045
  }
10652
11046
 
10653
11047
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/snapshot") {
11048
+ if (!authUser?.userId) {
11049
+ json(res, 401, { error: "Authentication required" });
11050
+ return;
11051
+ }
10654
11052
  let payload;
10655
11053
  try {
10656
11054
  payload = JSON.parse(await readBody(req));
@@ -10815,6 +11213,10 @@ export function startUiServer({
10815
11213
  }
10816
11214
 
10817
11215
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/action") {
11216
+ if (!authUser?.userId) {
11217
+ json(res, 401, { error: "Authentication required" });
11218
+ return;
11219
+ }
10818
11220
  let payload;
10819
11221
  try {
10820
11222
  payload = JSON.parse(await readBody(req));
@@ -11237,6 +11639,10 @@ export function startUiServer({
11237
11639
  }
11238
11640
 
11239
11641
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/idempotency") {
11642
+ if (!authUser?.userId) {
11643
+ json(res, 401, { error: "Authentication required" });
11644
+ return;
11645
+ }
11240
11646
  let payload;
11241
11647
  try {
11242
11648
  payload = JSON.parse(await readBody(req));
@@ -11304,7 +11710,117 @@ export function startUiServer({
11304
11710
  return;
11305
11711
  }
11306
11712
 
11713
+ if (req.method === "POST" && url.pathname === "/api/workflows/report") {
11714
+ if (!authUser?.userId) {
11715
+ json(res, 401, { error: "Authentication required" });
11716
+ return;
11717
+ }
11718
+ let payload;
11719
+ try {
11720
+ payload = JSON.parse(await readBody(req));
11721
+ } catch {
11722
+ json(res, 400, { error: "Invalid JSON body" });
11723
+ return;
11724
+ }
11725
+ try {
11726
+ const report = normalizeWorkflowReport(payload);
11727
+ if (report.error) {
11728
+ json(res, 400, { error: report.error });
11729
+ return;
11730
+ }
11731
+ if (report.workflow.namespace !== "tapd") {
11732
+ json(res, 400, { error: `Unsupported workflow namespace: ${report.workflow.namespace}` });
11733
+ return;
11734
+ }
11735
+ const tapdId = report.workflow.id;
11736
+ const flowId = report.flowId;
11737
+ const flowSource = report.flowSource || "user";
11738
+ const archived = payload.archived === true || payload.flowArchived === true;
11739
+ const workflowScope = resolvePrdWorkflowScope(root, {
11740
+ ...payload,
11741
+ tapdId,
11742
+ flowId,
11743
+ flowSource,
11744
+ archived,
11745
+ }, userCtx, "write");
11746
+ if (workflowScope.error) {
11747
+ json(res, workflowScope.status || 400, { error: workflowScope.error });
11748
+ return;
11749
+ }
11750
+ const scopedRoot = workflowScope.stateRoot;
11751
+ prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
11752
+ const currentSnapshot = prdWorkflowMaterializeSnapshot(
11753
+ workflowScope.executionRoot,
11754
+ scopedRoot,
11755
+ tapdId,
11756
+ userCtx,
11757
+ { flowSource, flowId },
11758
+ );
11759
+ const acceptedRevisions = new Set([
11760
+ String(currentSnapshot.runtimeRevision || "").trim(),
11761
+ String(currentSnapshot.revision || "").trim(),
11762
+ ].filter(Boolean));
11763
+ if (report.expectedRevision && acceptedRevisions.size && !acceptedRevisions.has(report.expectedRevision)) {
11764
+ json(res, 409, {
11765
+ error: "Workflow state changed; refresh before reporting",
11766
+ conflict: {
11767
+ type: "workflow-revision-conflict",
11768
+ expectedRevision: report.expectedRevision,
11769
+ currentRevision: currentSnapshot.runtimeRevision || currentSnapshot.revision || "",
11770
+ workflow: report.workflow,
11771
+ },
11772
+ snapshot: currentSnapshot,
11773
+ });
11774
+ return;
11775
+ }
11776
+ if (report.idempotencyKey) {
11777
+ const existing = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, report.idempotencyKey);
11778
+ if (existing) {
11779
+ json(res, 200, {
11780
+ ok: true,
11781
+ alreadyApplied: true,
11782
+ report,
11783
+ event: existing,
11784
+ snapshot: currentSnapshot,
11785
+ });
11786
+ return;
11787
+ }
11788
+ }
11789
+ const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
11790
+ ...report.event,
11791
+ tapdId,
11792
+ actor: {
11793
+ userId: String(userCtx.userId || ""),
11794
+ username: String(authUser.username || userCtx.userId || ""),
11795
+ },
11796
+ });
11797
+ if (!event) throw new Error("Failed to store workflow report");
11798
+ const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
11799
+ prdWorkflowMaterializeSnapshot(
11800
+ workflowScope.executionRoot,
11801
+ scopedRoot,
11802
+ tapdId,
11803
+ userCtx,
11804
+ { flowSource, flowId },
11805
+ ),
11806
+ getSessionTokenFromRequest(req) || "",
11807
+ );
11808
+ prdWorkflowBroadcast(
11809
+ prdWorkflowKey(userCtx, flowSource, flowId, tapdId),
11810
+ { type: "workflow-report", tapdId, workflow: report.workflow, event, snapshot },
11811
+ );
11812
+ json(res, 200, { ok: true, report, event, snapshot });
11813
+ } catch (e) {
11814
+ json(res, 500, { error: (e && e.message) || String(e) });
11815
+ }
11816
+ return;
11817
+ }
11818
+
11307
11819
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/event") {
11820
+ if (!authUser?.userId) {
11821
+ json(res, 401, { error: "Authentication required" });
11822
+ return;
11823
+ }
11308
11824
  let payload;
11309
11825
  try {
11310
11826
  payload = JSON.parse(await readBody(req));
@@ -11359,6 +11875,10 @@ export function startUiServer({
11359
11875
  }
11360
11876
 
11361
11877
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/review-link") {
11878
+ if (!authUser?.userId) {
11879
+ json(res, 401, { error: "Authentication required" });
11880
+ return;
11881
+ }
11362
11882
  let payload;
11363
11883
  try {
11364
11884
  payload = JSON.parse(await readBody(req));
@@ -11439,7 +11959,18 @@ export function startUiServer({
11439
11959
  const tapdId = String(url.searchParams.get("tapdId") || "").trim();
11440
11960
  const flowId = String(url.searchParams.get("flowId") || "").trim();
11441
11961
  const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
11442
- const key = prdWorkflowKey(userCtx, flowSource, flowId, tapdId);
11962
+ const workflowShare = String(url.searchParams.get("workflowShare") || "").trim();
11963
+ const workflowScope = resolvePrdWorkflowScope(root, {
11964
+ tapdId,
11965
+ flowId,
11966
+ flowSource,
11967
+ workflowShare,
11968
+ }, userCtx);
11969
+ if (workflowScope.error) {
11970
+ json(res, workflowScope.status || 400, { error: workflowScope.error });
11971
+ return;
11972
+ }
11973
+ const key = prdWorkflowKey(userCtx, flowSource, flowId, tapdId, workflowShare);
11443
11974
  let set = prdWorkflowSubscribers.get(key);
11444
11975
  if (!set) {
11445
11976
  set = new Set();
@@ -11483,6 +12014,7 @@ export function startUiServer({
11483
12014
  flowSource,
11484
12015
  archived,
11485
12016
  workspaceId: url.searchParams.get("workspaceId") || "",
12017
+ workflowShare: url.searchParams.get("workflowShare") || "",
11486
12018
  }, userCtx);
11487
12019
  if (workflowScope.error) {
11488
12020
  res.writeHead(workflowScope.status || 400, { "Content-Type": "text/plain; charset=utf-8" });
@@ -13850,7 +14382,10 @@ export function startUiServer({
13850
14382
 
13851
14383
  if (req.method === "GET" && url.pathname === "/api/ui-context") {
13852
14384
  try {
13853
- json(res, 200, { workspaceRoot: root, ...uiConfig });
14385
+ json(res, 200, {
14386
+ ...uiConfig,
14387
+ ...(authUser?.isAdmin ? { workspaceRoot: root } : {}),
14388
+ });
13854
14389
  } catch (e) {
13855
14390
  json(res, 500, { error: (e && e.message) || String(e) });
13856
14391
  }
@@ -13920,6 +14455,10 @@ export function startUiServer({
13920
14455
  }
13921
14456
 
13922
14457
  if (req.method === "GET" && url.pathname === "/api/agentflow-config") {
14458
+ if (!authUser?.isAdmin) {
14459
+ json(res, 403, { error: "Admin permission required" });
14460
+ return;
14461
+ }
13923
14462
  try {
13924
14463
  const cfg = readAgentflowUserConfigObject();
13925
14464
  const opencodeProvider = typeof cfg.opencodeProvider === "string" ? cfg.opencodeProvider : "";
@@ -13931,6 +14470,10 @@ export function startUiServer({
13931
14470
  }
13932
14471
 
13933
14472
  if (req.method === "POST" && url.pathname === "/api/agentflow-config") {
14473
+ if (!authUser?.isAdmin) {
14474
+ json(res, 403, { error: "Admin permission required" });
14475
+ return;
14476
+ }
13934
14477
  let payload;
13935
14478
  try {
13936
14479
  payload = JSON.parse(await readBody(req));
@@ -14020,8 +14563,11 @@ export function startUiServer({
14020
14563
 
14021
14564
  if (req.method === "GET" && url.pathname === "/api/user-env") {
14022
14565
  try {
14566
+ const userEnvRows = readUserEnvRows(userCtx.userId);
14023
14567
  json(res, 200, {
14024
- env: readUserEnvRows(userCtx.userId),
14568
+ env: authUser?.isAdmin
14569
+ ? userEnvRows
14570
+ : userEnvRows.filter((row) => !ADMIN_ONLY_USER_ENV_KEYS.has(String(row?.key || "").trim())),
14025
14571
  globalEnv: authUser?.isAdmin ? readGlobalEnvRows() : [],
14026
14572
  canEditGlobalEnv: Boolean(authUser?.isAdmin),
14027
14573
  });
@@ -14044,13 +14590,23 @@ export function startUiServer({
14044
14590
  json(res, 403, { error: "Admin permission required" });
14045
14591
  return;
14046
14592
  }
14047
- const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
14593
+ const requestedEnvRows = Array.isArray(payload?.env) ? payload.env : [];
14594
+ if (!authUser?.isAdmin && requestedEnvRows.some((row) => ADMIN_ONLY_USER_ENV_KEYS.has(String(row?.key || "").trim()))) {
14595
+ json(res, 403, { error: "Admin permission required for infrastructure environment keys" });
14596
+ return;
14597
+ }
14598
+ const preservedAdminRows = authUser?.isAdmin
14599
+ ? []
14600
+ : readUserEnvRows(userCtx.userId).filter((row) => ADMIN_ONLY_USER_ENV_KEYS.has(String(row?.key || "").trim()));
14601
+ const envRows = writeUserEnvRows(userCtx.userId, [...preservedAdminRows, ...requestedEnvRows]);
14048
14602
  const globalEnvRows = authUser?.isAdmin && Object.prototype.hasOwnProperty.call(payload || {}, "globalEnv")
14049
14603
  ? writeGlobalEnvRows(payload?.globalEnv || [])
14050
14604
  : readGlobalEnvRows();
14051
14605
  json(res, 200, {
14052
14606
  success: true,
14053
- env: envRows,
14607
+ env: authUser?.isAdmin
14608
+ ? envRows
14609
+ : envRows.filter((row) => !ADMIN_ONLY_USER_ENV_KEYS.has(String(row?.key || "").trim())),
14054
14610
  globalEnv: authUser?.isAdmin ? globalEnvRows : [],
14055
14611
  canEditGlobalEnv: Boolean(authUser?.isAdmin),
14056
14612
  });
@@ -14061,6 +14617,10 @@ export function startUiServer({
14061
14617
  }
14062
14618
 
14063
14619
  if (req.method === "POST" && url.pathname === "/api/update-model-lists") {
14620
+ if (!authUser?.isAdmin) {
14621
+ json(res, 403, { error: "Admin permission required" });
14622
+ return;
14623
+ }
14064
14624
  try {
14065
14625
  let opencodeProviderOverride = "";
14066
14626
  const raw = await readBody(req);