@playcraft/cli 0.1.56-beta → 0.1.58-beta

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.
Files changed (101) hide show
  1. package/dist/assets/client.js +6 -4
  2. package/dist/commands/tools-3d.js +15 -0
  3. package/dist/commands/tools-generation.js +3 -2
  4. package/dist/commands/workspace.js +43 -0
  5. package/dist/utils/agent-api-client.js +31 -8
  6. package/dist/utils/agent-generation-provenance.js +45 -0
  7. package/dist/utils/generation-workspace-scope.js +50 -0
  8. package/dist/utils/image-generation-retry.js +7 -0
  9. package/dist/utils/tool-operation-journal.js +34 -4
  10. package/dist/workspace/messages.js +10 -0
  11. package/dist/workspace-runtime/batch/deliver.js +140 -80
  12. package/dist/workspace-runtime/batch/freeze.js +18 -6
  13. package/dist/workspace-runtime/batch/generate.js +68 -64
  14. package/dist/workspace-runtime/batch/prepare.js +109 -137
  15. package/dist/workspace-runtime/batch/records.js +4 -0
  16. package/dist/workspace-runtime/batch/routes.js +10 -2
  17. package/dist/workspace-runtime/batch/source-candidate.js +2 -0
  18. package/dist/workspace-runtime/batch/source-capture.js +1 -0
  19. package/dist/workspace-runtime/batch/source-failure.js +2 -0
  20. package/dist/workspace-runtime/batch/source-publish.js +3 -0
  21. package/dist/workspace-runtime/batch/writer-proof.js +37 -0
  22. package/dist/workspace-runtime/codex/app-server.js +1 -0
  23. package/dist/workspace-runtime/codex/config-toml.js +4 -0
  24. package/dist/workspace-runtime/codex/delivery-tools.js +7 -5
  25. package/dist/workspace-runtime/codex/generation-provenance-broker.js +80 -0
  26. package/dist/workspace-runtime/codex/launcher-client.js +18 -0
  27. package/dist/workspace-runtime/codex/model-broker.js +23 -4
  28. package/dist/workspace-runtime/persistence/journal.js +34 -0
  29. package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
  30. package/dist/workspace-runtime/previews/output.js +2 -0
  31. package/dist/workspace-runtime/previews/publish.js +20 -5
  32. package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
  33. package/dist/workspace-runtime/processes/managed-writes.js +47 -5
  34. package/dist/workspace-runtime/serve.js +19 -32
  35. package/dist/workspace-runtime/server/accept-operation.js +22 -2
  36. package/dist/workspace-runtime/server/directory-access.js +414 -0
  37. package/dist/workspace-runtime/server/dispatch.js +61 -23
  38. package/dist/workspace-runtime/server/endpoints.js +1 -0
  39. package/dist/workspace-runtime/server/http.js +52 -10
  40. package/dist/workspace-runtime/server/operation-record.js +2 -0
  41. package/dist/workspace-runtime/server/terminal-host.js +43 -8
  42. package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
  43. package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
  44. package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
  45. package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
  46. package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
  47. package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
  48. package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
  49. package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
  50. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
  51. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
  52. package/dist/workspace-runtime/workspaces/attachments.js +9 -0
  53. package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
  54. package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
  55. package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
  56. package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
  57. package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
  58. package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
  59. package/dist/workspace-runtime/workspaces/dependencies.js +105 -71
  60. package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
  61. package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
  62. package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
  63. package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
  64. package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
  65. package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
  66. package/dist/workspace-runtime/workspaces/files.js +4 -0
  67. package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
  68. package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
  69. package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
  70. package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
  71. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
  72. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
  73. package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
  74. package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
  75. package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
  76. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
  77. package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
  78. package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
  79. package/dist/workspace-runtime/workspaces/parameters.js +95 -3
  80. package/dist/workspace-runtime/workspaces/platform-export.js +190 -0
  81. package/dist/workspace-runtime/workspaces/prepare.js +4 -0
  82. package/dist/workspace-runtime/workspaces/registry.js +11 -0
  83. package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
  84. package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
  85. package/dist/workspace-runtime/workspaces/skills.js +14 -15
  86. package/dist/workspace-runtime/workspaces/transfer.js +4 -1
  87. package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
  88. package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
  89. package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
  90. package/package.json +5 -4
  91. package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
  92. package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
  93. package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
  94. package/project-template-v2/.playcraft/skills.lock.json +15 -8
  95. package/templates/preview-runtime/v1/README.md +4 -0
  96. package/templates/preview-runtime/v1/manifest.json +6 -6
  97. package/templates/preview-runtime/v1/runtime.mjs +368 -22
  98. package/templates/preview-runtime/v1/types/adapters.d.ts +4 -1
  99. package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
  100. package/templates/preview-runtime/v1/types/protocol.d.ts +19 -0
  101. package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
@@ -120,7 +120,9 @@ export async function downloadAssetTask(client, scope, taskId, output, versionId
120
120
  const staging = await mkdtemp(join(dirname(destination), '.playcraft-asset-'));
121
121
  let createdDestination = false;
122
122
  try {
123
- const files = bundle?.files ?? [{ ...entry, path: 'output', fileId: undefined }];
123
+ const multiple = !bundle && !versionId && Boolean(task.outputs);
124
+ const directory = Boolean(bundle || multiple);
125
+ const files = bundle?.files ?? (multiple ? task.outputs.map(file => ({ ...file, path: file.name })) : [{ ...entry, path: 'output', fileId: undefined }]);
124
126
  const paths = [];
125
127
  for (const file of files) {
126
128
  const temporary = resolve(staging, file.path);
@@ -128,11 +130,11 @@ export async function downloadAssetTask(client, scope, taskId, output, versionId
128
130
  if (!inside || inside === '..' || inside.startsWith(`..${sep}`))
129
131
  throw new Error(t('invalidContent'));
130
132
  await mkdir(dirname(temporary), { recursive: true });
131
- await client.download(assetContentRoute({ ...scope, taskId, ...(bundle ? { fileId: file.fileId } : {}), ...(versionId ? { versionId } : {}) }), temporary);
133
+ await client.download(assetContentRoute({ ...scope, taskId, ...(directory ? { fileId: file.fileId } : {}), ...(versionId ? { versionId } : {}) }), temporary);
132
134
  await verifyFile(temporary, file);
133
- paths.push(bundle ? join(destination, file.path) : destination);
135
+ paths.push(directory ? join(destination, file.path) : destination);
134
136
  }
135
- if (bundle) {
137
+ if (directory) {
136
138
  // mkdir is exclusive; an existing directory or symbolic link cannot be replaced.
137
139
  await mkdir(destination);
138
140
  createdDestination = true;
@@ -46,6 +46,7 @@ export function register3DCommands(tools) {
46
46
  .description('Generate a 3D model from a text prompt or input image (uses HunyuanTo3D or configured provider)')
47
47
  .option('--prompt <text>', 'Text description of the 3D model to generate')
48
48
  .option('--input <path>', 'Input image (local path or URL) for image-to-3D generation')
49
+ .option('--view-image <view=path>', 'Additional local reference for hy-3d-3.1: left|right|back|top|bottom|left_front|right_front; repeat for multiple views', (value, previous) => [...previous, value], [])
49
50
  .requiredOption('--output <path>', 'Output model file path')
50
51
  .option('--model <ref>', 'Model ref in provider/model-id format (e.g. tencent-cloud/hy-3d-3.1)')
51
52
  .option('--format <fmt>', 'Output format: glb|obj|stl|fbx|usdz (default: glb)', 'glb')
@@ -77,6 +78,20 @@ export function register3DCommands(tools) {
77
78
  payload.enablePBR = true;
78
79
  if (opts.polygonType)
79
80
  payload.polygonType = opts.polygonType;
81
+ if (opts.viewImage.length) {
82
+ const views = new Set(['left', 'right', 'back', 'top', 'bottom', 'left_front', 'right_front']);
83
+ const seen = new Set();
84
+ if (!opts.input || opts.viewImage.length > 7 || opts.model && opts.model !== 'tencent-cloud/hy-3d-3.1')
85
+ throw new Error('--view-image requires --input and hy-3d-3.1 (at most 7 unique views)');
86
+ payload.multiViewImages = opts.viewImage.map((value) => {
87
+ const separator = value.indexOf('='), view = value.slice(0, separator), imagePath = value.slice(separator + 1);
88
+ if (separator < 1 || !imagePath || !views.has(view) || seen.has(view))
89
+ throw new Error('--view-image expects a unique view=path, e.g. back=./back.png');
90
+ seen.add(view);
91
+ return { view: view,
92
+ imageBase64: readFileSync(imagePath).toString('base64'), mimeType: mimeTypeForImagePath(imagePath) };
93
+ });
94
+ }
80
95
  if (opts.input) {
81
96
  let inputImageBase64;
82
97
  let inputImageMimeType = opts.inputMime;
@@ -6,6 +6,7 @@ import { captureGeneratedArtifact } from '../workspace-runtime/workspaces/artifa
6
6
  import { resolveImageBackgroundRequirement, validateImageDeliveryOptions } from '@playcraft/common/prefab';
7
7
  import { assertImageAssetOutputPaths, readImageAssetContext } from '../utils/image-asset-context.js';
8
8
  import { AgentApiClient } from '../utils/agent-api-client.js';
9
+ import { isUnsafeImageRetryError } from '../utils/image-generation-retry.js';
9
10
  import { MAX_REFERENCE_IMAGES, collectReferenceImagePaths, parseImageTargetFieldPath, collectReferenceImagePayloads, sniffImageExtension, extensionForImageMime, resolveImageOutputPath, handleError, raceWithTimeout, } from './tools-utils.js';
10
11
  function isMasterCompositeStripRequest(opts) {
11
12
  if (opts.aspectRatio === '45:16')
@@ -285,7 +286,7 @@ export function registerGenerationCommands(tools) {
285
286
  lastError = e;
286
287
  const msg = e instanceof Error ? e.message : String(e);
287
288
  const isTimeout = msg.includes('timed out');
288
- const cannotRetry = /source_transparency_invalid|output_transparency_invalid|transparency_unsupported|"retryable"\s*:\s*false/.test(msg);
289
+ const cannotRetry = isUnsafeImageRetryError(e);
289
290
  if (isTimeout || cannotRetry) {
290
291
  console.error(`Attempt ${attempt + 1} stopped: ${msg}`);
291
292
  break;
@@ -433,7 +434,7 @@ export function registerGenerationCommands(tools) {
433
434
  tools.command('generate-sfx')
434
435
  .description('AI 生成音效(SFX),使用 ElevenLabs Sound Effects')
435
436
  .requiredOption('--prompt <text>', '音效描述,仅英文,如 "crisp UI click, short tail, no voice"')
436
- .option('--duration <seconds>', '时长(秒,0.5-30)', parseFloat)
437
+ .option('--duration <seconds>', '时长(秒,0.5-22;省略时由供应商自动决定)', parseFloat)
437
438
  .option('--loop', '生成可循环的音效', false)
438
439
  .requiredOption('--output <path>', '保存路径,如 ./assets/audio/click.mp3')
439
440
  .action(async (opts) => {
@@ -1,3 +1,6 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { diagnoseWorkspaceShares, WORKSPACE_SHARED_PATHS } from '../workspace-runtime/workspaces/workspace-shares.js';
1
4
  import { workspaceClone, workspacePush } from '../workspace/transfer-client.js';
2
5
  import { workspaceMessage as message } from '../workspace/messages.js';
3
6
  import { normalizeWorkspaceTransferError, WorkspaceCliTransferError } from '../workspace/transfer-errors.js';
@@ -22,7 +25,47 @@ export function registerWorkspaceCommands(program) {
22
25
  process.exitCode = 1;
23
26
  }
24
27
  };
28
+ const doctor = async (options) => {
29
+ const root = resolve(options.dir ?? process.cwd());
30
+ const info = await stat(root).catch(() => undefined);
31
+ if (!info?.isDirectory()) {
32
+ if (options.json)
33
+ console.log(JSON.stringify({ ok: false, error: 'workspace_root_missing', root }));
34
+ else
35
+ console.error(message('doctorRootMissing'));
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const drift = await diagnoseWorkspaceShares(root);
40
+ if (options.json) {
41
+ console.log(JSON.stringify({
42
+ ok: drift.length === 0,
43
+ root,
44
+ shared: [...WORKSPACE_SHARED_PATHS],
45
+ drift: drift.map((entry) => ({
46
+ path: entry.path.slice(root.length + 1),
47
+ missing: entry.missing,
48
+ owner: entry.owner,
49
+ })),
50
+ }));
51
+ }
52
+ else if (drift.length === 0) {
53
+ console.log(message('doctorHealthy'));
54
+ }
55
+ else {
56
+ for (const entry of drift) {
57
+ console.log(`${entry.missing}\t${entry.owner}\t${entry.path.slice(root.length + 1)}`);
58
+ }
59
+ console.log(message('doctorSummary', { count: drift.length }));
60
+ console.log(message('doctorHint'));
61
+ }
62
+ process.exitCode = drift.length === 0 ? 0 : 1;
63
+ };
25
64
  const workspace = program.command('workspace').description(message('description'));
65
+ workspace.command('doctor').description(message('doctor'))
66
+ .option('--dir <path>', message('doctorTarget'), process.cwd())
67
+ .option('--json', message('json'))
68
+ .action(doctor);
26
69
  workspace.command('clone').description(message('clone'))
27
70
  .requiredOption('--workspace-id <id>', message('workspaceId'))
28
71
  .option('--directory <name>', message('directory'), 'basic')
@@ -9,8 +9,10 @@ import { homedir } from 'os';
9
9
  import { loadGlobalConfig } from '../config.js';
10
10
  import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
11
11
  import { AGENT_TOOL_OPERATION_PROTOCOL, AGENT_TOOL_OPERATION_HEADERS, AGENT_TOOL_OPERATION_CAPABILITIES_PATH, } from '@playcraft/common/agent-tools';
12
- import { beginToolOperation } from './tool-operation-journal.js';
12
+ import { beginToolOperation, findToolOperation } from './tool-operation-journal.js';
13
13
  import { businessEndpoint } from './environment-endpoints.js';
14
+ import { cloudRuntimeCredentialHint, isWorkspaceGenerationPath, withGenerationWorkspaceScope } from './generation-workspace-scope.js';
15
+ import { agentGenerationProvenanceHeaders } from './agent-generation-provenance.js';
14
16
  import { redactDiagnosticText } from './error-diagnostics.js';
15
17
  import { AccountActivationError, parseErrorPayload, pendingAccountActivationError, } from '../auth/account-activation.js';
16
18
  /**
@@ -37,6 +39,7 @@ export class AgentApiClient {
37
39
  token;
38
40
  credentialFile;
39
41
  playcraftEnv;
42
+ cloudRuntimeCredential;
40
43
  constructor() {
41
44
  const localConfig = AgentApiClient.loadLocalConfig();
42
45
  const globalConfig = loadGlobalConfig();
@@ -59,6 +62,7 @@ export class AgentApiClient {
59
62
  this.token = token;
60
63
  this.credentialFile = localConfig.credentialFile;
61
64
  this.playcraftEnv = (process.env.PLAYCRAFT_ENV || localConfig.env || '').trim();
65
+ this.cloudRuntimeCredential = cloudRuntimeCredentialHint(token, token === localConfig.token ? localConfig : undefined);
62
66
  }
63
67
  /** 根据 token 类型构造鉴权 headers */
64
68
  authHeaders() {
@@ -85,12 +89,26 @@ export class AgentApiClient {
85
89
  }
86
90
  async post(path, body, options) {
87
91
  const url = this.toolsRequestUrl(path);
88
- const serializedBody = JSON.stringify(body);
89
- if (serializedBody === undefined)
92
+ const originalBody = JSON.stringify(body);
93
+ if (originalBody === undefined)
90
94
  throw new Error('Tool request body is required');
91
95
  const recoverId = options?.requestId ?? process.env.PLAYCRAFT_TOOL_REQUEST_ID;
96
+ const newRequest = options?.newRequest ?? process.env.PLAYCRAFT_TOOL_NEW_REQUEST === '1';
97
+ const originalReceipt = recoverId !== undefined && !newRequest && isWorkspaceGenerationPath(path)
98
+ ? findToolOperation(url.pathname, originalBody, recoverId) : undefined;
99
+ let serializedBody = originalReceipt ? originalBody : JSON.stringify(await withGenerationWorkspaceScope(path, body, this.cloudRuntimeCredential));
100
+ if (serializedBody === undefined)
101
+ throw new Error('Tool request body is required');
102
+ if (serializedBody !== originalBody && !newRequest) {
103
+ const previous = findToolOperation(url.pathname, originalBody, recoverId);
104
+ if (previous && recoverId !== undefined)
105
+ serializedBody = originalBody;
106
+ else if (previous)
107
+ throw new Error(`Previous unscoped tool request ${previous.requestId} is still unconfirmed. ` +
108
+ `Rerun this command with PLAYCRAFT_TOOL_REQUEST_ID=${previous.requestId} to read its settled receipt before starting a workspace generation. No generation was sent.`);
109
+ }
92
110
  const operation = beginToolOperation(url.pathname, serializedBody, recoverId, {
93
- newRequest: options?.newRequest ?? process.env.PLAYCRAFT_TOOL_NEW_REQUEST === '1',
111
+ newRequest,
94
112
  });
95
113
  const unknownOutcome = `Tool request ${operation.requestId}: the outcome is unknown. Rerun the same command to ` +
96
114
  `continue this request: it returns the result if the request finished, or finishes it if ` +
@@ -135,6 +153,7 @@ export class AgentApiClient {
135
153
  headers: {
136
154
  'Content-Type': 'application/json',
137
155
  ...this.authHeaders(),
156
+ ...await agentGenerationProvenanceHeaders(path, serializedBody, operation.requestId, this.cloudRuntimeCredential, options?.signal),
138
157
  [AGENT_TOOL_OPERATION_HEADERS.protocol]: AGENT_TOOL_OPERATION_PROTOCOL,
139
158
  [AGENT_TOOL_OPERATION_HEADERS.requestId]: operation.requestId,
140
159
  ...(operation.settledOnly ? { [AGENT_TOOL_OPERATION_HEADERS.settledOnly]: 'true' } : {}),
@@ -163,11 +182,15 @@ export class AgentApiClient {
163
182
  const pending = pendingAccountActivationError(res.status, parseErrorPayload(text));
164
183
  if (pending)
165
184
  throw pending;
185
+ if (isWorkspaceGenerationPath(path) && res.headers.get('x-playcraft-generation-outcome') === 'failed') {
186
+ operation.reject();
187
+ throw new ToolRequestError(`API ${res.status}: ${text}`);
188
+ }
166
189
  const code = errorCode(text);
167
- if (res.status === 409 && code === 'business_operation_in_progress')
190
+ if (res.status === 409 && (code === 'business_operation_in_progress' || code === 'asset_generation_in_progress'))
168
191
  throw new ToolRequestError(`API 409: ${text}\nTool request ${operation.requestId} is still running. Rerun the ` +
169
192
  'same command later to get its result.');
170
- if (res.status === 409 && code === 'business_operation_requires_reconciliation')
193
+ if (res.status === 409 && (code === 'business_operation_requires_reconciliation' || code === 'asset_generation_outcome_unknown'))
171
194
  throw new ToolRequestError(recoverId !== undefined
172
195
  ? // A receipt query never runs the request, even for a tool that can be safely redone.
173
196
  `API 409: ${text}\nTool request ${operation.requestId} has no settled result yet. ` +
@@ -331,8 +354,8 @@ function responseErrorReason(text) {
331
354
  }
332
355
  function errorCode(text) {
333
356
  try {
334
- const message = JSON.parse(text).message;
335
- return typeof message === 'string' ? message : undefined;
357
+ const payload = JSON.parse(text);
358
+ return typeof payload.code === 'string' ? payload.code : typeof payload.message === 'string' ? payload.message : undefined;
336
359
  }
337
360
  catch {
338
361
  return undefined;
@@ -0,0 +1,45 @@
1
+ import { hashOperationPayload, AGENT_GENERATION_PROVENANCE_HEADER } from '@playcraft/common/cloud-workspace/runtime';
2
+ import { isWorkspaceGenerationPath } from './generation-workspace-scope.js';
3
+ /** Attribution is optional; only the controlled local broker can attest an execution. */
4
+ export async function agentGenerationProvenanceHeaders(path, body, requestId, cloudRuntime, signal) {
5
+ if (!cloudRuntime)
6
+ return {};
7
+ const normalized = path.startsWith('/') ? path : `/${path}`;
8
+ let payload;
9
+ try {
10
+ payload = JSON.parse(body);
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ const assetCreate = normalized === '/workspace-assets' && payload?.action === 'create';
16
+ if (!isWorkspaceGenerationPath(normalized) && !assetCreate)
17
+ return {};
18
+ const endpoint = process.env.PLAYCRAFT_AGENT_GENERATION_ENDPOINT;
19
+ const capability = process.env.PLAYCRAFT_AGENT_GENERATION_CAPABILITY;
20
+ if (!endpoint || !capability || capability.length > 128 || endpoint.length > 1024)
21
+ return {};
22
+ try {
23
+ const url = new URL(endpoint);
24
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password
25
+ || url.search || url.hash || !/\/[0-9a-f-]{36}\/[0-9a-f]{32}\/generation-provenance$/.test(url.pathname))
26
+ return {};
27
+ const response = await fetch(url, {
28
+ method: 'POST', redirect: 'error',
29
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
30
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${capability}` },
31
+ body: JSON.stringify({ toolPath: `/agent/tools${normalized}`,
32
+ toolRequestId: assetCreate ? payload.clientRequestId : requestId, payloadHash: hashOperationPayload(payload) }),
33
+ });
34
+ if (!response.ok) {
35
+ await response.body?.cancel();
36
+ return {};
37
+ }
38
+ const value = await response.json();
39
+ return typeof value.proof === 'string' && value.proof.length > 0 && value.proof.length <= 8192
40
+ ? { [AGENT_GENERATION_PROVENANCE_HEADER]: value.proof } : {};
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import { basename, dirname } from 'node:path';
2
+ import { workspaceDirectoryRegistrationSchema } from '@playcraft/common/cloud-workspace/runtime';
3
+ import { generatedWorkspace } from '../workspace-runtime/workspaces/artifact-receipts.js';
4
+ const generationPaths = new Set([
5
+ '/generate-image', '/generate-sfx', '/generate-bgm', '/generate-3d',
6
+ '/generate-texture', '/generate-motion', '/generate-avatar',
7
+ ]);
8
+ const workspaceScopeSchema = workspaceDirectoryRegistrationSchema.pick({ workspaceId: true, directoryName: true });
9
+ export function isWorkspaceGenerationPath(path) {
10
+ return generationPaths.has(path.startsWith('/') ? path : `/${path}`);
11
+ }
12
+ /** Credential metadata only selects routing; the business service still authenticates and authorizes it. */
13
+ export function cloudRuntimeCredentialHint(token, metadata) {
14
+ if (token.startsWith('playcraft_pat_'))
15
+ return false;
16
+ if (metadata?.kind === 'cloud-runtime-cli' || typeof metadata?.runtimeId === 'string' && metadata.runtimeId.trim())
17
+ return true;
18
+ const parts = token.split('.');
19
+ if (parts.length !== 3 || parts[1].length > 16 * 1024)
20
+ return false;
21
+ try {
22
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
23
+ return Boolean(payload && typeof payload === 'object' && 'kind' in payload && payload.kind === 'cloud-runtime-cli');
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** Add the current engineering checkout before the durable operation hashes its request. */
30
+ export async function withGenerationWorkspaceScope(path, body, requiresWorkspace, cwd = process.cwd()) {
31
+ if (!isWorkspaceGenerationPath(path))
32
+ return body;
33
+ const context = await generatedWorkspace(cwd);
34
+ if (!context) {
35
+ if (requiresWorkspace)
36
+ throw new Error('Cloud workspace generation requires a current engineering directory; run this command from basic or a remix directory. No generation was sent.');
37
+ return body;
38
+ }
39
+ const parsed = workspaceScopeSchema.safeParse({ workspaceId: context.workspaceId, directoryName: basename(context.root) });
40
+ if (!parsed.success || basename(dirname(context.root)) !== context.workspaceId)
41
+ throw new Error('Cloud workspace generation requires a valid basic or remix checkout under its workspace. No generation was sent.');
42
+ if (!body || typeof body !== 'object' || Array.isArray(body))
43
+ throw new Error('Tool generation request must be an object');
44
+ if ('workspace' in body && body.workspace !== undefined) {
45
+ const explicit = workspaceScopeSchema.safeParse(body.workspace);
46
+ if (!explicit.success || explicit.data.workspaceId !== parsed.data.workspaceId || explicit.data.directoryName !== parsed.data.directoryName)
47
+ throw new Error('Tool generation workspace does not match the current engineering directory. No generation was sent.');
48
+ }
49
+ return { ...body, workspace: parsed.data };
50
+ }
@@ -0,0 +1,7 @@
1
+ /** An uncertain generation may still be billed; retrying must never create another operation. */
2
+ export function isUnsafeImageRetryError(error) {
3
+ const message = error instanceof Error ? error.message : String(error);
4
+ return /source_transparency_invalid|output_transparency_invalid|transparency_unsupported|"retryable"\s*:\s*false/.test(message)
5
+ || /business_operation_(?:in_progress|requires_reconciliation)|asset_generation_(?:in_progress|outcome_unknown)/.test(message)
6
+ || /outcome is unknown|outcome remains unconfirmed|still unconfirmed|needs reconciliation|no settled result|settled result is not available/i.test(message);
7
+ }
@@ -97,9 +97,40 @@ export function beginToolOperation(path, serializedBody, recoverId, options = {}
97
97
  release: () => inFlight.delete(requestId),
98
98
  };
99
99
  }
100
- function findUnconfirmed(directory, path, inputHash) {
100
+ /** Read an existing identity without creating an intent or changing its receipt state. */
101
+ export function findToolOperation(path, serializedBody, requestId) {
102
+ const directory = process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests');
103
+ if (!isAbsolute(directory))
104
+ throw new Error('PLAYCRAFT_TOOL_REQUEST_DIR must be an absolute path');
105
+ const inputHash = createHash('sha256').update(serializedBody).digest('hex');
106
+ if (requestId === undefined)
107
+ return findUnconfirmed(directory, path, inputHash, true);
108
+ if (!isAgentToolRequestId(requestId))
109
+ return undefined;
110
+ try {
111
+ const entry = JSON.parse(readFileSync(join(directory, `${requestId.toLowerCase()}.json`), 'utf8'));
112
+ if (entry.version === 1 && entry.requestId === requestId.toLowerCase() && entry.path === path && entry.inputHash === inputHash
113
+ && ['unconfirmed', 'completed', 'rejected'].includes(entry.state))
114
+ return entry;
115
+ }
116
+ catch (error) {
117
+ if (error.code !== 'ENOENT')
118
+ throw error;
119
+ }
120
+ return undefined;
121
+ }
122
+ function findUnconfirmed(directory, path, inputHash, includeActive = false) {
101
123
  let latest;
102
- for (const name of readdirSync(directory)) {
124
+ let files;
125
+ try {
126
+ files = readdirSync(directory);
127
+ }
128
+ catch (error) {
129
+ if (error.code === 'ENOENT')
130
+ return undefined;
131
+ throw error;
132
+ }
133
+ for (const name of files) {
103
134
  if (!name.endsWith('.json'))
104
135
  continue;
105
136
  try {
@@ -112,8 +143,7 @@ function findUnconfirmed(directory, path, inputHash) {
112
143
  (!latest || entry.updatedAt > latest.updatedAt) &&
113
144
  // Still being sent, here or by another live process: that run is not interrupted.
114
145
  // Checked last, so only this command's candidates cost a process probe.
115
- !inFlight.has(entry.requestId) &&
116
- !holderAlive(entry.holder))
146
+ (includeActive || !inFlight.has(entry.requestId) && !holderAlive(entry.holder)))
117
147
  latest = entry;
118
148
  }
119
149
  catch {
@@ -31,6 +31,11 @@ const messages = {
31
31
  preflight: 'Push preflight passed: {files} files, {bytes} of content. Preparing incremental transfer.',
32
32
  upload: 'Uploading incremental transfer: {bytes}.',
33
33
  login: 'Log in to the target environment using playcraft login --url',
34
+ doctor: 'List shared workspace paths that lost development-group access',
35
+ doctorTarget: 'Workspace checkout to inspect', doctorRootMissing: 'The target directory does not exist.',
36
+ doctorHealthy: 'All shared workspace paths are readable and writable by the development group.',
37
+ doctorSummary: '{count} shared path(s) need repair.',
38
+ doctorHint: 'Reconnect the workspace to repair entries owned by this user; entries owned by the other identity are repaired when that identity writes again.',
34
39
  },
35
40
  zh: {
36
41
  description: '下载云工作区,并将本地提交推送回来', clone: '克隆当前工作区文件,包含尚未提交的修改',
@@ -59,6 +64,11 @@ const messages = {
59
64
  preflight: '推送预检查通过:{files} 个文件,内容共 {bytes}。正在准备增量传输。',
60
65
  upload: '正在上传增量传输包:{bytes}。',
61
66
  login: '请使用 playcraft login --url 登录目标环境',
67
+ doctor: '列出失去开发组访问权限的共享工作区路径',
68
+ doctorTarget: '要检查的工作区目录', doctorRootMissing: '目标目录不存在。',
69
+ doctorHealthy: '所有共享工作区路径都能被开发组读写。',
70
+ doctorSummary: '有 {count} 个共享路径需要修复。',
71
+ doctorHint: '重新连接工作区可修复属于当前用户的条目;属于另一个身份的条目会在该身份下次写入时修复。',
62
72
  },
63
73
  };
64
74
  export function workspaceMessage(key, values = {}) {