@playcraft/cli 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,6 +141,17 @@ playcraft login --url https://your-playcraft.example.com
141
141
 
142
142
  登录成功后,`tools`、`recommend`、`fix-ids` 等所有需要后端 API 的命令均无需额外配置,直接使用。
143
143
 
144
+ 账号提示待激活时,使用同一目标环境及已保存的个人访问令牌主动激活:
145
+
146
+ ```bash
147
+ playcraft login --activate
148
+ playcraft login --activate --url https://your-playcraft.example.com
149
+ ```
150
+
151
+ 也可通过 `PLAYCRAFT_TOKEN` 提供个人访问令牌。激活只发送一次请求,成功后重新执行原业务命令;该动作不会打开浏览器或续签令牌。目标服务须已接入身份服务的激活入口;封禁、停用账号及沙箱执行令牌不能用于激活。
152
+
153
+ 若本地、全局或环境变量配置冲突,激活会要求明确指定 `--url`、所需的 `PLAYCRAFT_ENV` 和对应的个人访问令牌。
154
+
144
155
  ### 沙箱环境(CI / Agent)
145
156
 
146
157
  在沙箱或 CI 中可通过 `.playcraft.json` 或环境变量配置:
@@ -0,0 +1,157 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { loadGlobalConfig } from '../config.js';
5
+ import { accountActivationMessage } from './messages.js';
6
+ export const ACCOUNT_PENDING_ACTIVATION = 'account_pending_activation';
7
+ export class AccountActivationError extends Error {
8
+ code;
9
+ status;
10
+ retryable = false;
11
+ constructor(code, message, status) {
12
+ super(message);
13
+ this.code = code;
14
+ this.status = status;
15
+ this.name = 'AccountActivationError';
16
+ }
17
+ }
18
+ export function responseErrorCode(payload) {
19
+ if (!payload || typeof payload !== 'object')
20
+ return undefined;
21
+ const { error, code } = payload;
22
+ return typeof error?.code === 'string' ? error.code : typeof code === 'string' ? code : undefined;
23
+ }
24
+ export function pendingAccountActivationError(status, payload) {
25
+ return status === 403 && responseErrorCode(payload) === ACCOUNT_PENDING_ACTIVATION
26
+ ? new AccountActivationError(ACCOUNT_PENDING_ACTIVATION, `${ACCOUNT_PENDING_ACTIVATION}: ${accountActivationMessage('pending')}`, status)
27
+ : undefined;
28
+ }
29
+ export function parseErrorPayload(text) {
30
+ try {
31
+ return JSON.parse(text);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function localActivationConfig() {
38
+ for (const path of [
39
+ join(process.cwd(), '.playcraft.json'),
40
+ '/project/.playcraft.json',
41
+ join(homedir(), '.playcraft.json'),
42
+ ]) {
43
+ if (!existsSync(path))
44
+ continue;
45
+ try {
46
+ const config = JSON.parse(readFileSync(path, 'utf8'));
47
+ if (config && typeof config === 'object' && !Array.isArray(config))
48
+ return config;
49
+ }
50
+ catch {
51
+ // Follow the existing local credential lookup when a file is unreadable.
52
+ }
53
+ }
54
+ return {};
55
+ }
56
+ function stringValue(value) {
57
+ return typeof value === 'string' && value ? value : undefined;
58
+ }
59
+ function activationBaseUrl(raw) {
60
+ try {
61
+ const url = new URL(raw);
62
+ if (!['http:', 'https:'].includes(url.protocol) ||
63
+ url.username ||
64
+ url.password ||
65
+ url.search ||
66
+ url.hash)
67
+ throw new Error();
68
+ url.pathname = url.pathname.replace(/\/+$/, '').replace(/\/api$/, '');
69
+ return url.toString().replace(/\/+$/, '');
70
+ }
71
+ catch {
72
+ throw new AccountActivationError('INVALID_ACTIVATION_TARGET', accountActivationMessage('invalidTarget'));
73
+ }
74
+ }
75
+ function savedTokenForTarget(config, rawUrl, target, environment) {
76
+ if (!rawUrl || (stringValue(config.env)?.trim() || '') !== environment)
77
+ return undefined;
78
+ try {
79
+ return activationBaseUrl(rawUrl) === target ? stringValue(config.token) : undefined;
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
85
+ function normalizedSavedUrl(raw) {
86
+ try {
87
+ return raw ? activationBaseUrl(raw) : undefined;
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ }
93
+ export async function activateAccount(options, defaultBaseUrl) {
94
+ const local = localActivationConfig();
95
+ const global = loadGlobalConfig();
96
+ const localUrl = stringValue(local.apiUrl);
97
+ const globalUrl = stringValue(global.backendUrl) || stringValue(global.url);
98
+ const explicitUrl = options.url || process.env.PLAYCRAFT_API_URL || process.env.PLAYCRAFT_URL;
99
+ const target = activationBaseUrl(explicitUrl || localUrl || globalUrl || defaultBaseUrl);
100
+ const explicitToken = options.token || process.env.PLAYCRAFT_TOKEN || process.env.PLAYCRAFT_SANDBOX_TOKEN;
101
+ const savedUrls = [normalizedSavedUrl(localUrl), normalizedSavedUrl(globalUrl)].filter(Boolean);
102
+ const savedTokens = [stringValue(local.token), stringValue(global.token)].filter(Boolean);
103
+ const environmentsConflict = savedUrls.length === 2 &&
104
+ savedUrls[0] === savedUrls[1] &&
105
+ (stringValue(local.env)?.trim() || '') !== (stringValue(global.env)?.trim() || '');
106
+ const environmentTokensConflict = process.env.PLAYCRAFT_TOKEN &&
107
+ process.env.PLAYCRAFT_SANDBOX_TOKEN &&
108
+ process.env.PLAYCRAFT_TOKEN !== process.env.PLAYCRAFT_SANDBOX_TOKEN;
109
+ const environmentUrlsConflict = process.env.PLAYCRAFT_API_URL &&
110
+ process.env.PLAYCRAFT_URL &&
111
+ normalizedSavedUrl(process.env.PLAYCRAFT_API_URL) !==
112
+ normalizedSavedUrl(process.env.PLAYCRAFT_URL);
113
+ if ((!explicitUrl && new Set(savedUrls).size > 1) ||
114
+ (!explicitToken && new Set(savedTokens).size > 1) ||
115
+ (environmentsConflict && !process.env.PLAYCRAFT_ENV) ||
116
+ (environmentTokensConflict && !options.token) ||
117
+ (environmentUrlsConflict && !options.url)) {
118
+ throw new AccountActivationError('AMBIGUOUS_ACTIVATION_TARGET', accountActivationMessage('ambiguous'));
119
+ }
120
+ const environment = (process.env.PLAYCRAFT_ENV ||
121
+ (!explicitUrl ? (localUrl ? stringValue(local.env) : stringValue(global.env)) : '') ||
122
+ '').trim();
123
+ const token = explicitToken ||
124
+ savedTokenForTarget(local, localUrl, target, environment) ||
125
+ savedTokenForTarget(global, globalUrl, target, environment);
126
+ if (!token)
127
+ throw new AccountActivationError('AUTH_REQUIRED', accountActivationMessage('missingToken'));
128
+ if (!/^playcraft_pat_[A-Za-z0-9_-]{32}$/.test(token)) {
129
+ throw new AccountActivationError('INVALID_ACTIVATION_TOKEN', accountActivationMessage('invalidToken'));
130
+ }
131
+ let response;
132
+ try {
133
+ response = await fetch(`${target}/api/settings/personal-access-tokens/activate`, {
134
+ method: 'POST',
135
+ redirect: 'error',
136
+ signal: AbortSignal.timeout(30_000),
137
+ headers: {
138
+ Authorization: `Bearer ${token}`,
139
+ ...(environment ? { 'playcraft-env': environment } : {}),
140
+ },
141
+ });
142
+ }
143
+ catch {
144
+ throw new AccountActivationError('ACTIVATION_UNCONFIRMED', accountActivationMessage('unavailable'));
145
+ }
146
+ if (!response.ok) {
147
+ await response.body?.cancel().catch(() => { });
148
+ throw new AccountActivationError('ACTIVATION_FAILED', accountActivationMessage('failed'), response.status);
149
+ }
150
+ const result = await response.json().catch(() => undefined);
151
+ if (!result ||
152
+ typeof result !== 'object' ||
153
+ result.accountStatus !== 'active') {
154
+ throw new AccountActivationError('ACTIVATION_UNCONFIRMED', accountActivationMessage('unavailable'), response.status);
155
+ }
156
+ console.log(accountActivationMessage('success'));
157
+ }
@@ -0,0 +1,30 @@
1
+ const messages = {
2
+ en: {
3
+ activateOption: 'Explicitly activate the account using the current personal access token',
4
+ tokenOption: 'Personal access token for --activate (or use PLAYCRAFT_TOKEN)',
5
+ pending: 'Account activation is required. Run `playcraft login --activate` with the same token and target environment, then rerun your command.',
6
+ success: 'Account activated. Rerun your original command to continue.',
7
+ missingToken: 'No personal access token matches this target environment. Provide PLAYCRAFT_TOKEN or log in to this environment first.',
8
+ invalidToken: 'Account activation requires a personal access token. A sandbox token cannot activate an account.',
9
+ ambiguous: 'Activation credentials or target environments conflict. Specify --url, PLAYCRAFT_ENV when needed, and an explicit personal access token using --token or PLAYCRAFT_TOKEN.',
10
+ invalidTarget: 'Use a web service base address (http:// or https://) without credentials, a query, or a fragment.',
11
+ failed: 'Account activation failed. Check the target environment and token before trying again.',
12
+ unavailable: 'Account activation could not be confirmed. Check your account status before explicitly trying again.',
13
+ },
14
+ zh: {
15
+ activateOption: '明确使用当前个人访问令牌激活账号',
16
+ tokenOption: '用于 --activate 的个人访问令牌(也可使用 PLAYCRAFT_TOKEN)',
17
+ pending: '账号需要激活。请使用同一令牌和目标环境主动运行 `playcraft login --activate`,然后重新执行原命令。',
18
+ success: '账号已激活,请重新执行原命令继续。',
19
+ missingToken: '没有与目标环境匹配的个人访问令牌。请提供 PLAYCRAFT_TOKEN,或先登录此环境。',
20
+ invalidToken: '激活账号需要个人访问令牌,沙箱令牌不能用于激活账号。',
21
+ ambiguous: '激活凭据或目标环境存在冲突。请明确指定 --url、所需的 PLAYCRAFT_ENV,并通过 --token 或 PLAYCRAFT_TOKEN 提供个人访问令牌。',
22
+ invalidTarget: '激活目标必须是网页服务基址(http:// 或 https://),不能包含凭据、查询参数或片段。',
23
+ failed: '账号激活失败,请检查目标环境和令牌后再尝试。',
24
+ unavailable: '无法确认账号是否已激活。请先检查账号状态,再主动决定是否重试。',
25
+ },
26
+ };
27
+ export function accountActivationMessage(key) {
28
+ const locale = process.env.PLAYCRAFT_LANGUAGE || process.env.LANG || 'en';
29
+ return messages[locale.startsWith('zh') ? 'zh' : 'en'][key];
30
+ }
@@ -2,6 +2,7 @@ import http from 'http';
2
2
  import { exec } from 'child_process';
3
3
  import crypto from 'crypto';
4
4
  import { saveGlobalConfig, loadGlobalConfig } from '../config.js';
5
+ import { activateAccount } from '../auth/account-activation.js';
5
6
  /** 默认云端 Portal(Next.js `/cli-auth` 与 API 同源时的基址;可通过 --url 或 PLAYCRAFT_URL 覆盖) */
6
7
  export const DEFAULT_PLAYCRAFT_CLOUD_BASE = 'https://playcraft.aix.intlgame.com';
7
8
  const CALLBACK_PORT = 3456;
@@ -100,6 +101,10 @@ export async function renewExistingToken(options) {
100
101
  }
101
102
  }
102
103
  export async function loginCommand(options) {
104
+ if (options.activate) {
105
+ await activateAccount(options, DEFAULT_PLAYCRAFT_CLOUD_BASE);
106
+ return;
107
+ }
103
108
  const backendUrl = resolveLoginBaseUrl(options);
104
109
  const callbackPort = options.callbackPort ?? CALLBACK_PORT;
105
110
  const globalConfig = loadGlobalConfig();
@@ -14,7 +14,7 @@ export async function runWorkspaceRuntimeServe(configPath) {
14
14
  return;
15
15
  }
16
16
  try {
17
- const runtime = await startWorkspaceRuntime(config);
17
+ const runtime = await startWorkspaceRuntime(config, { configPath });
18
18
  console.log(JSON.stringify({
19
19
  status: 'listening',
20
20
  url: runtime.url,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import { accountActivationMessage } from './auth/messages.js';
3
4
  import { readFileSync } from 'fs';
4
5
  import { fileURLToPath } from 'url';
5
6
  import { dirname, join } from 'path';
@@ -66,6 +67,8 @@ program
66
67
  .command('login')
67
68
  .description('登录 PlayCraft 账号并获取 CLI token')
68
69
  .option('--url <url>', 'Portal / API 基址(默认 https://playcraft.aix.intlgame.com,可用环境变量 PLAYCRAFT_URL 覆盖)')
70
+ .option('--activate', accountActivationMessage('activateOption'))
71
+ .option('--token <token>', accountActivationMessage('tokenOption'))
69
72
  .action(async (options) => {
70
73
  const { loginCommand } = await import('./commands/login.js');
71
74
  await loginCommand(options);
@@ -5,6 +5,7 @@ import { loadGlobalConfig } from '../config.js';
5
5
  import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
6
6
  import { AGENT_TOOL_OPERATION_PROTOCOL, AGENT_TOOL_OPERATION_HEADERS, AGENT_TOOL_OPERATION_CAPABILITIES_PATH, } from '@playcraft/common/agent-tools';
7
7
  import { beginToolOperation } from './tool-operation-journal.js';
8
+ import { AccountActivationError, parseErrorPayload, pendingAccountActivationError, } from '../auth/account-activation.js';
8
9
  /**
9
10
  * 判断 token 是否为用户 PAT(Personal Access Token)。
10
11
  * PAT 以 "playcraft_pat_" 开头;沙箱 JWT 为三段式 base64。
@@ -86,6 +87,14 @@ export class AgentApiClient {
86
87
  signal: AbortSignal.timeout(30_000),
87
88
  });
88
89
  if (!capabilityResponse.ok) {
90
+ if (capabilityResponse.status === 403) {
91
+ const pending = pendingAccountActivationError(403, parseErrorPayload(await capabilityResponse.clone().text().catch(() => '')));
92
+ if (pending) {
93
+ if (operation.createdNew && !operation.settledOnly)
94
+ operation.reject();
95
+ throw pending;
96
+ }
97
+ }
89
98
  await capabilityResponse.body?.cancel();
90
99
  throw new ToolRequestError(`Business tool protocol unavailable (${capabilityResponse.status}); no operation was sent`);
91
100
  }
@@ -108,6 +117,14 @@ export class AgentApiClient {
108
117
  });
109
118
  if (res.headers.get(AGENT_TOOL_OPERATION_HEADERS.protocol) !== AGENT_TOOL_OPERATION_PROTOCOL ||
110
119
  res.headers.get(AGENT_TOOL_OPERATION_HEADERS.requestId) !== operation.requestId) {
120
+ if (res.status === 403) {
121
+ const pending = pendingAccountActivationError(403, parseErrorPayload(await res.clone().text().catch(() => '')));
122
+ if (pending) {
123
+ if (operation.createdNew && !operation.settledOnly)
124
+ operation.reject();
125
+ throw pending;
126
+ }
127
+ }
111
128
  await res.body?.cancel();
112
129
  // Possibly an instance that ignores identities: a rerun may only read the receipt.
113
130
  operation.requireReceipt();
@@ -115,6 +132,9 @@ export class AgentApiClient {
115
132
  }
116
133
  if (!res.ok) {
117
134
  const text = await res.text().catch(() => res.statusText);
135
+ const pending = pendingAccountActivationError(res.status, parseErrorPayload(text));
136
+ if (pending)
137
+ throw pending;
118
138
  const code = errorCode(text);
119
139
  if (res.status === 409 && code === 'business_operation_in_progress')
120
140
  throw new ToolRequestError(`API 409: ${text}\nTool request ${operation.requestId} is still running. Rerun the ` +
@@ -141,7 +161,7 @@ export class AgentApiClient {
141
161
  return result;
142
162
  }
143
163
  catch (error) {
144
- if (error instanceof ToolRequestError)
164
+ if (error instanceof ToolRequestError || error instanceof AccountActivationError)
145
165
  throw error;
146
166
  throw new Error(`${error instanceof Error ? error.message : String(error)}\n${operation.settledOnly
147
167
  ? `Tool request ${operation.requestId}; record: ${operation.file}. Its settled result is not available yet.`
@@ -164,6 +184,9 @@ export class AgentApiClient {
164
184
  });
165
185
  if (!res.ok) {
166
186
  const text = await res.text().catch(() => res.statusText);
187
+ const pending = pendingAccountActivationError(res.status, parseErrorPayload(text));
188
+ if (pending)
189
+ throw pending;
167
190
  throw new Error(`API ${res.status}: ${text}`);
168
191
  }
169
192
  return res.json();
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { loadGlobalConfig } from '../config.js';
3
3
  import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
4
+ import { pendingAccountActivationError } from '../auth/account-activation.js';
4
5
  export class PlayCraftApiError extends Error {
5
6
  code;
6
7
  status;
@@ -95,19 +96,20 @@ export class PlayCraftHttpClient {
95
96
  }
96
97
  if (!response.ok) {
97
98
  const error = payload?.error;
98
- const code = typeof error?.code === 'string' ? error.code : statusCode(response.status);
99
- const message = typeof error?.message === 'string'
99
+ const pending = pendingAccountActivationError(response.status, payload);
100
+ const code = pending?.code ?? (typeof error?.code === 'string' ? error.code : statusCode(response.status));
101
+ const message = pending?.message ?? (typeof error?.message === 'string'
100
102
  ? error.message
101
103
  : typeof payload?.message === 'string'
102
104
  ? payload.message
103
105
  : typeof payload === 'string'
104
106
  ? payload
105
- : response.statusText || 'PlayCraft request failed';
107
+ : response.statusText || 'PlayCraft request failed');
106
108
  throw new PlayCraftApiError({
107
109
  code,
108
110
  message,
109
111
  status: response.status,
110
- details: error?.details ?? payload,
112
+ details: pending ? { loginCommand: 'playcraft login --activate' } : error?.details ?? payload,
111
113
  retryable: response.status >= 500,
112
114
  });
113
115
  }
@@ -56,8 +56,10 @@ export function beginToolOperation(path, serializedBody, recoverId, options = {}
56
56
  createdAt: now,
57
57
  updatedAt: now,
58
58
  };
59
+ let createdNew = false;
59
60
  try {
60
61
  writeFileSync(file, JSON.stringify(entry), { flag: 'wx', mode: 0o600 });
62
+ createdNew = true;
61
63
  }
62
64
  catch (error) {
63
65
  if (error.code !== 'EEXIST')
@@ -81,6 +83,8 @@ export function beginToolOperation(path, serializedBody, recoverId, options = {}
81
83
  update({ holder });
82
84
  return {
83
85
  requestId,
86
+ /** This call created the identity; a refusal cannot settle an older unknown operation. */
87
+ createdNew,
84
88
  /** An explicit recovery, or a server that did not acknowledge the identity, reads only. */
85
89
  settledOnly: recoverId !== undefined || unconfirmed?.receiptOnly === true,
86
90
  file,
@@ -58,7 +58,7 @@ export async function runPreviewPublish(state, operation, root) {
58
58
  const outputDir = join(root, OUTPUT_FOLDER, operation.operationId);
59
59
  const output = { log: '' };
60
60
  try {
61
- await ensureDependencies(state, workspaceId, root);
61
+ await ensureDependencies(state, workspaceId, root, output);
62
62
  await excludeFromGit(root, `/${OUTPUT_FOLDER}/`);
63
63
  await excludeFromGit(root, `/${BUILDER_TEMP_FOLDER}/`);
64
64
  await buildPreview(state, root, outputDir, operation.operationId, output);
@@ -137,11 +137,13 @@ export function previewFailureCode(error) {
137
137
  * Codex added since the workspace opened is installed first. A running install is joined rather
138
138
  * than started twice; a failed one is retried once here.
139
139
  */
140
- async function ensureDependencies(state, workspaceId, root) {
140
+ async function ensureDependencies(state, workspaceId, root, output) {
141
141
  await scheduleDependencyInstall(state, workspaceId, root);
142
- const status = readDependencies(state, workspaceId).status;
143
- if (status !== 'ready' && status !== 'none')
142
+ const dependencies = readDependencies(state, workspaceId);
143
+ if (dependencies.status !== 'ready' && dependencies.status !== 'none') {
144
+ output.log = [dependencies.error, dependencies.logTail].filter(Boolean).join('\n');
144
145
  throw new PreviewFailure('dependencies_not_ready');
146
+ }
145
147
  }
146
148
  async function buildPreview(state, root, outputDir, operationId, output) {
147
149
  // Tests replace the build; the real one needs a project and the published CLI.
@@ -33,11 +33,15 @@ export async function startWorkspaceRuntime(config, options = {}) {
33
33
  try {
34
34
  journal = new RuntimeJournal(config.stateDir);
35
35
  await journal.load();
36
+ const previousEpoch = journal.storedRuntimeEpoch();
37
+ const previousGroup = journal.executionGroup();
38
+ const previousIdentityMatches = journal.epochsSnapshot().some((epoch) => epoch.runtime_id === config.runtimeId && epoch.generation === config.generation && epoch.runtime_epoch === previousEpoch);
36
39
  await recoverInterruptedInstalls(journal);
37
40
  await recoverInterruptedPreviews(journal);
38
41
  await mkdir(config.workspaceRoot, { recursive: true, mode: 0o700 });
39
42
  const state = {
40
43
  config: { ...config, protocolMinor: negotiated.protocolMinor },
44
+ configPath: options.configPath,
41
45
  journal,
42
46
  acceptedLeaseFence: journal.acceptedLeaseFence(),
43
47
  gitAvailable: await probeGitBinary(),
@@ -63,8 +67,28 @@ export async function startWorkspaceRuntime(config, options = {}) {
63
67
  try {
64
68
  await native?.start?.();
65
69
  const group = native?.executionProcess?.();
66
- if (group)
67
- await journal.noteExecutionGroup(group);
70
+ if (group) {
71
+ let previousStopped = false;
72
+ if (previousGroup.pid && previousGroup.pid !== group.pid) {
73
+ try {
74
+ process.kill(previousGroup.pid, 0);
75
+ }
76
+ catch (error) {
77
+ previousStopped = error.code === 'ESRCH';
78
+ }
79
+ }
80
+ // The supervisor advances the trusted config epoch only after stopping the old
81
+ // native group. Reconfirm its recorded PID before publishing that replacement.
82
+ if (previousIdentityMatches && previousEpoch !== undefined && config.runtimeEpoch > previousEpoch && previousStopped) {
83
+ await journal.writeExecutionEpoch({
84
+ runtimeEpoch: state.runtimeEpoch, ...group, previousPid: previousGroup.pid, stopConfirmed: true,
85
+ });
86
+ await journal.replaceSessions(journal.listSessions().map((session) => ({ ...session, recoverability: 'history_only' })));
87
+ }
88
+ else {
89
+ await journal.noteExecutionGroup(group);
90
+ }
91
+ }
68
92
  }
69
93
  catch (error) {
70
94
  const code = error instanceof Error ? error.message : '';
@@ -767,7 +767,7 @@ async function executeTerminalCreate(state, scopeKey, operation, payload) {
767
767
  }
768
768
  /** The branch a registered workspace is bound to; the single-workspace mode has none. */
769
769
  function boundBranch(state, operation) {
770
- return operation.workspaceId ? state.journal.getWorkspace(operation.workspaceId)?.branchName : undefined;
770
+ return operation.workspaceId ? state.journal.getWorkspace(operation.workspaceId)?.branchName ?? undefined : undefined;
771
771
  }
772
772
  function gitErrorCode(error) {
773
773
  return error instanceof GitWorkspaceError ? error.code : 'git_command_failed';
@@ -0,0 +1,19 @@
1
+ import { chmod, rename, writeFile } from 'node:fs/promises';
2
+ import { applySandboxEndpointPatch } from '@playcraft/common/cloud-workspace/runtime';
3
+ /** Persist the new addresses, then publish them to the running process. A restart is not required. */
4
+ export async function applyRuntimeEndpointPatch(state, patch) {
5
+ const next = applySandboxEndpointPatch(state.config, patch);
6
+ const changed = next.callbackUrl !== state.config.callbackUrl || next.modelProxyUrl !== state.config.modelProxyUrl;
7
+ const result = { changed, callbackUrl: next.callbackUrl, modelProxyUrl: next.modelProxyUrl };
8
+ if (!changed)
9
+ return result;
10
+ if (!state.configPath) {
11
+ throw new Error('endpoint_config_path_missing');
12
+ }
13
+ const temporary = `${state.configPath}.${process.pid}.tmp`;
14
+ await writeFile(temporary, `${JSON.stringify(next)}\n`, { mode: 0o600 });
15
+ await chmod(temporary, 0o600);
16
+ await rename(temporary, state.configPath);
17
+ state.config = next;
18
+ return result;
19
+ }
@@ -3,6 +3,7 @@ import { registerWorkspace, revisionScope, workspaceRoot, WorkspaceContextError
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { workspaceRegistrationRequestSchema, HTTP_STATUS_BY_ERROR, assertLeaseFence, buildHandshakeDocument, capability, findUntrustedRuntimeFields, gitFileRefSchema, hashOperationPayload, instanceRebuildRequestSchema, sourceAckSchema, operationAcceptRequestSchema, operationKindSchema, operationScopeKey, resolveIdempotentAccept, nativeHistoryReadSchema, runtimeError, unavailableRuntimeCapabilities, } from '@playcraft/common/cloud-workspace/runtime';
5
5
  import { controlTokenMatches, extractControlToken } from './auth.js';
6
+ import { applyRuntimeEndpointPatch } from './endpoints.js';
6
7
  import { canExecuteKind, executeOperation } from './dispatch.js';
7
8
  import { InvalidWorkspacePathError, listWorkspaceFiles, } from '../workspaces/files.js';
8
9
  import { materializeRemoteSnapshot } from '../workspaces/file-snapshot.js';
@@ -61,6 +62,15 @@ export async function handleRuntimeRequest(state, request, response) {
61
62
  writeJson(response, 200, { workspaceId: binding.workspaceId, status: 'registered' });
62
63
  return;
63
64
  }
65
+ if (request.method === 'POST' && url.pathname === '/runtime/endpoints') {
66
+ try {
67
+ writeJson(response, 200, await applyRuntimeEndpointPatch(state, await readJson(request)));
68
+ }
69
+ catch {
70
+ writeError(response, runtimeError('invalid_config', 'endpoint_patch_rejected'));
71
+ }
72
+ return;
73
+ }
64
74
  if (request.method === 'GET' && url.pathname === '/runtime/handshake') {
65
75
  writeJson(response, 200, handshake(state));
66
76
  return;
@@ -101,7 +111,11 @@ export async function handleRuntimeRequest(state, request, response) {
101
111
  if (request.method === 'GET' && url.pathname === '/runtime/files') {
102
112
  if (writeDestroyed(state, response))
103
113
  return;
104
- writeJson(response, 200, await listWorkspaceFiles(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined), state.journal.fileRevisions(revisionScope(state, url.searchParams.get('workspaceId') ?? undefined))));
114
+ const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
115
+ const root = await workspaceRoot(state, workspaceId);
116
+ const listed = await listWorkspaceFiles(root, state.journal.fileRevisions(revisionScope(state, workspaceId)));
117
+ console.log(`[workspace-files] GET /runtime/files workspaceId=${workspaceId ?? '-'} root=${root} files=${listed.files.length}`);
118
+ writeJson(response, 200, listed);
105
119
  return;
106
120
  }
107
121
  if (request.method === 'GET' && url.pathname === '/runtime/file') {
@@ -77,6 +77,8 @@ export class WorkspaceGitCredentialBroker {
77
77
  }
78
78
  async configure(binding, preparation) {
79
79
  preparation = workspacePreparationSchema.parse(preparation);
80
+ if (binding.targetKind === 'standalone' || preparation.mode !== 'clone')
81
+ throw new Error('git_access_denied');
80
82
  const transport = preparation.transport;
81
83
  const isDirect = isLocalWorkspaceGitAuthMode(preparation.authMode);
82
84
  if (!isDirect && transport?.kind !== 'git-gateway-v2')
@@ -118,7 +120,7 @@ export class WorkspaceGitCredentialBroker {
118
120
  || typeof input.path !== 'string')
119
121
  throw new Error('git_access_denied');
120
122
  const binding = this.state.journal.getWorkspace(input.workspaceId);
121
- if (!binding)
123
+ if (!binding || binding.targetKind === 'standalone')
122
124
  throw new Error('git_access_denied');
123
125
  const cloneUrl = `${input.protocol}://${input.host}/${input.path}`;
124
126
  const direct = await this.direct.get(binding);
@@ -177,6 +179,8 @@ export class WorkspaceGitCredentialBroker {
177
179
  throw new Error('git_broker_unavailable');
178
180
  }
179
181
  const preparation = workspacePreparationSchema.parse(await response.json());
182
+ if (preparation.mode !== 'clone')
183
+ throw new Error('git_access_denied');
180
184
  const matched = preparation.transport && matchWorkspaceGitGatewayCloneUrl(preparation.transport.cloneUrl, this.state.config.gitGatewayOrigins, binding.repositoryUrl);
181
185
  if (this.closed || this.state.journal.destroyed() || this.state.runtimeEpoch !== epoch
182
186
  || JSON.stringify(this.state.journal.getWorkspace(binding.workspaceId)) !== JSON.stringify(binding)
@@ -4,15 +4,19 @@ import { lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs
4
4
  import { join } from 'node:path';
5
5
  import { isLocalWorkspaceGitAuthMode, isLocalWorkspaceGitEnvironment, matchWorkspaceGitGatewayCloneUrl, workspacePreparationSchema, workspaceRegistrationSchema, } from '@playcraft/common/cloud-workspace/runtime';
6
6
  import { WorkspaceContextError } from './context-error.js';
7
- /** The caller holds the Workspace lock. Only an absent, never-published checkout may be cloned. */
7
+ /** The caller holds the Workspace lock. Only an absent, never-published directory may be prepared. */
8
8
  export async function prepareWorkspaceCheckout(state, binding, input, leaseFence) {
9
9
  const preparation = workspacePreparationSchema.safeParse(input);
10
10
  if (!preparation.success)
11
11
  throw new WorkspaceContextError('invalid_workspace_preparation');
12
- const source = parseCloneSource(binding.repositoryUrl);
13
- const direct = isLocalWorkspaceGitAuthMode(preparation.data.authMode);
14
- if (direct && (!isLocalWorkspaceGitEnvironment(state.config.environment) || !preparation.data.directAccess
15
- || preparation.data.directAccess.repositoryUrl !== binding.repositoryUrl))
12
+ const standalone = binding.targetKind === 'standalone';
13
+ if (standalone !== (preparation.data.mode === 'empty'))
14
+ throw new WorkspaceContextError('invalid_workspace_preparation');
15
+ const clone = preparation.data.mode === 'clone' ? preparation.data : undefined;
16
+ const source = binding.repositoryUrl ? parseCloneSource(binding.repositoryUrl) : undefined;
17
+ const direct = isLocalWorkspaceGitAuthMode(clone?.authMode);
18
+ if (direct && (!isLocalWorkspaceGitEnvironment(state.config.environment) || !clone?.directAccess
19
+ || clone.directAccess.repositoryUrl !== binding.repositoryUrl))
16
20
  throw new WorkspaceContextError('workspace_git_gateway_unavailable');
17
21
  const parent = await realpath(state.config.workspaceRoot);
18
22
  const target = join(parent, binding.workspaceId);
@@ -42,15 +46,15 @@ export async function prepareWorkspaceCheckout(state, binding, input, leaseFence
42
46
  await writeReceipt(receiptPath, receipt);
43
47
  await rm(staging, { recursive: true, force: true });
44
48
  await mkdir(staging, { mode: 0o700 });
45
- const access = preparation.data.gitAccess;
46
- const credential = preparation.data.directAccess ?? access;
49
+ const access = clone?.gitAccess;
50
+ const credential = clone?.directAccess ?? access;
47
51
  const now = Date.now();
48
52
  if (access &&
49
53
  (Date.parse(access.issuedAt) > now || Date.parse(access.expiresAt) <= now + 10_000)) {
50
54
  await rm(staging, { recursive: true, force: true });
51
55
  throw new WorkspaceContextError('workspace_git_access_expired');
52
56
  }
53
- const transport = preparation.data.transport;
57
+ const transport = clone?.transport;
54
58
  if (transport && !matchWorkspaceGitGatewayCloneUrl(transport.cloneUrl, state.config.gitGatewayOrigins, binding.repositoryUrl).ok) {
55
59
  await rm(staging, { recursive: true, force: true });
56
60
  throw new WorkspaceContextError('workspace_git_gateway_unavailable');
@@ -76,20 +80,25 @@ export async function prepareWorkspaceCheckout(state, binding, input, leaseFence
76
80
  }
77
81
  }
78
82
  try {
79
- await runGit(['check-ref-format', '--branch', binding.branchName], env, deadline);
80
- await runGit([
81
- 'clone',
82
- '--no-checkout',
83
- '--single-branch',
84
- '--branch',
85
- binding.branchName,
86
- '--',
87
- binding.repositoryUrl,
88
- staging,
89
- ], env, deadline);
90
- // Checkout never receives the temporary credential and never invokes repository hooks or global filters.
91
- clearCloneSecrets(env);
92
- await runGit(['-C', staging, 'checkout', '--force', binding.branchName], env, deadline);
83
+ if (standalone) {
84
+ await runGit(['init', '--initial-branch=main', staging], env, deadline);
85
+ }
86
+ else {
87
+ await runGit(['check-ref-format', '--branch', binding.branchName], env, deadline);
88
+ await runGit([
89
+ 'clone',
90
+ '--no-checkout',
91
+ '--single-branch',
92
+ '--branch',
93
+ binding.branchName,
94
+ '--',
95
+ binding.repositoryUrl,
96
+ staging,
97
+ ], env, deadline);
98
+ // Checkout never receives the temporary credential and never invokes repository hooks or global filters.
99
+ clearCloneSecrets(env);
100
+ await runGit(['-C', staging, 'checkout', '--force', binding.branchName], env, deadline);
101
+ }
93
102
  assertCurrent();
94
103
  receipt.status = 'ready';
95
104
  await writeReceipt(receiptPath, receipt);
@@ -46,7 +46,7 @@ async function checkedDirectory(state, workspaceId) {
46
46
  throw new WorkspaceContextError('workspace_not_prepared');
47
47
  }
48
48
  }
49
- /** Registers a fixed checkout; optional first creation is restricted to its trusted binding. */
49
+ /** Registers a fixed directory; optional first creation is restricted to its trusted binding. */
50
50
  export async function registerWorkspace(state, input, leaseFence, preparation) {
51
51
  if (state.config.workspaceMode !== 'multi')
52
52
  throw new WorkspaceContextError('multi_workspace_unavailable');
@@ -87,9 +87,18 @@ export async function registerWorkspace(state, input, leaseFence, preparation) {
87
87
  },
88
88
  })).stdout.trim();
89
89
  try {
90
- if ((await realpath(await git('rev-parse', '--show-toplevel'))) !== root ||
91
- (await git('config', '--get', 'remote.origin.url')) !== binding.repositoryUrl ||
92
- (await git('symbolic-ref', '--short', 'HEAD')) !== binding.branchName) {
90
+ let checkoutMatches;
91
+ if (binding.targetKind === 'standalone') {
92
+ const gitDirectory = await lstat(join(root, '.git'));
93
+ checkoutMatches = gitDirectory.isDirectory() && !gitDirectory.isSymbolicLink()
94
+ && (await git('rev-parse', '--absolute-git-dir')) === join(root, '.git')
95
+ && (await git('remote')) === '';
96
+ }
97
+ else {
98
+ checkoutMatches = (await git('config', '--get', 'remote.origin.url')) === binding.repositoryUrl
99
+ && (await git('symbolic-ref', '--short', 'HEAD')) === binding.branchName;
100
+ }
101
+ if ((await realpath(await git('rev-parse', '--show-toplevel'))) !== root || !checkoutMatches) {
93
102
  throw new WorkspaceContextError('workspace_checkout_mismatch');
94
103
  }
95
104
  }
@@ -102,7 +111,7 @@ export async function registerWorkspace(state, input, leaseFence, preparation) {
102
111
  throw new WorkspaceContextError('runtime_destroyed');
103
112
  if (leaseFence < state.acceptedLeaseFence)
104
113
  throw new WorkspaceContextError('stale_lease');
105
- if (preparation?.transport?.kind === 'git-gateway-v2' || isLocalWorkspaceGitAuthMode(preparation?.authMode)) {
114
+ if (preparation?.mode === 'clone' && (preparation.transport?.kind === 'git-gateway-v2' || isLocalWorkspaceGitAuthMode(preparation.authMode))) {
106
115
  if (!state.gitCredentials)
107
116
  throw new WorkspaceContextError('workspace_git_gateway_unavailable');
108
117
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,8 +48,8 @@
48
48
  "@gltf-transform/core": "^4.3.0",
49
49
  "@gltf-transform/extensions": "^4.3.0",
50
50
  "@gltf-transform/functions": "^4.3.0",
51
- "@playcraft/build": "^0.1.9",
52
- "@playcraft/common": "^0.1.9",
51
+ "@playcraft/build": "^0.1.10",
52
+ "@playcraft/common": "^0.1.10",
53
53
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
54
54
  "chokidar": "^4.0.3",
55
55
  "commander": "^13.1.0",
@@ -4,147 +4,147 @@
4
4
  {
5
5
  "skillRef": "platform/playcraft-3d-flip-sprite",
6
6
  "version": "1.0.0",
7
- "artifactHash": "bfea71413bf9f0b57eda4c79c1c9e5d73061becaf9c1ece066ffcc32890ad1ea",
7
+ "artifactHash": "95183007861a4dae5b67440ea9beed1c360eb616d17e4aa04b97383cc2b63516",
8
8
  "surface": "portal-agent",
9
9
  "mountPath": ".playcraft/skills/platform__playcraft-3d-flip-sprite"
10
10
  },
11
11
  {
12
12
  "skillRef": "platform/playcraft-asset-management",
13
13
  "version": "1.0.0",
14
- "artifactHash": "11ae4c6b1e9e9cb8639e22cfc9568f70cc7fc526bb9632e601e2886ba50fb0f3",
14
+ "artifactHash": "27209dba24f6e1851dc83682fbd24165894c802794c797cac32d8871f740cf14",
15
15
  "surface": "portal-agent",
16
16
  "mountPath": ".playcraft/skills/platform__playcraft-asset-management"
17
17
  },
18
18
  {
19
19
  "skillRef": "platform/playcraft-build",
20
20
  "version": "1.0.0",
21
- "artifactHash": "7fbd4ba060efc9409e8da9aa7d4edfe2cdb2734de7a497b79299a7061cacf3ac",
21
+ "artifactHash": "2ca96916041bcc04ee48e827599dd819072e67a626bf59a1c20281f337e4063a",
22
22
  "surface": "portal-agent",
23
23
  "mountPath": ".playcraft/skills/platform__playcraft-build"
24
24
  },
25
25
  {
26
26
  "skillRef": "platform/playcraft-code-editor",
27
27
  "version": "1.0.0",
28
- "artifactHash": "4db2b7e60762ba0ccc249ea4023dcb81701dac38d54af0e6757b5337ea489a32",
28
+ "artifactHash": "abcc9aafe2b15adb8de62eb89921dbcb35c7dc634055b937fe34bdf83ca6685d",
29
29
  "surface": "portal-agent",
30
30
  "mountPath": ".playcraft/skills/platform__playcraft-code-editor"
31
31
  },
32
32
  {
33
33
  "skillRef": "platform/playcraft-create-remix",
34
34
  "version": "1.0.0",
35
- "artifactHash": "76f7cfd4b7441184805e07550f687651225a3baf27e306f0ee7c4347e58f3f19",
35
+ "artifactHash": "8d2a4d4d096433ccec090f1003c050471ddfbc50d69c1c0c66f88424b52bacf6",
36
36
  "surface": "portal-agent",
37
37
  "mountPath": ".playcraft/skills/platform__playcraft-create-remix"
38
38
  },
39
39
  {
40
40
  "skillRef": "platform/playcraft-deploy",
41
- "version": "1.0.0",
42
- "artifactHash": "e619cee8bea40b2a0c9b69a02d387369a3628a73c0a0504b3c92b1330bc12e86",
41
+ "version": "1.0.1",
42
+ "artifactHash": "31ac7f90d93606eb40c2113d56204e09b97ecd9e49b910b2ce05445b824ccfdc",
43
43
  "surface": "portal-agent",
44
44
  "mountPath": ".playcraft/skills/platform__playcraft-deploy"
45
45
  },
46
46
  {
47
47
  "skillRef": "platform/playcraft-iteration-methodology",
48
- "version": "1.0.0",
49
- "artifactHash": "42c5219f6f8a8d63787efeef6da8522cf7400442dc75dc8dee6d0c49b0ed28b7",
48
+ "version": "1.1.0",
49
+ "artifactHash": "f67a2f1806e08a48a903408383a06802622da3760d24d72a0dcbbea202349be2",
50
50
  "surface": "portal-agent",
51
51
  "mountPath": ".playcraft/skills/platform__playcraft-iteration-methodology"
52
52
  },
53
53
  {
54
54
  "skillRef": "platform/playcraft-platform-intro",
55
55
  "version": "1.0.0",
56
- "artifactHash": "d32001cff62516d343707fdbe4812ed9dde6651c749f2aadc848d06196f07c15",
56
+ "artifactHash": "b37f6b0515a79822f5f69519fcddd1182d420b90eb9ba40298bb1ed73eced281",
57
57
  "surface": "portal-agent",
58
58
  "mountPath": ".playcraft/skills/platform__playcraft-platform-intro"
59
59
  },
60
60
  {
61
61
  "skillRef": "platform/playcraft-prefab",
62
62
  "version": "1.0.0",
63
- "artifactHash": "7089e5b346ce99dbc25f61e2645e67fce057e6a108445975c99a03b062c84007",
63
+ "artifactHash": "a4a991302922d6846a2e313dc4cd4514f54820040dc2166ccc864803053c1ea5",
64
64
  "surface": "portal-agent",
65
65
  "mountPath": ".playcraft/skills/platform__playcraft-prefab"
66
66
  },
67
67
  {
68
68
  "skillRef": "platform/playcraft-project-management",
69
69
  "version": "1.0.0",
70
- "artifactHash": "32459786cdd17ece2df094be1f13387802325191531939c40c1ddfde3590a17b",
70
+ "artifactHash": "e7e4bffe80e0737362a5cac4234056a7d7fb048f0c8b5e41041e61ee914495e3",
71
71
  "surface": "portal-agent",
72
72
  "mountPath": ".playcraft/skills/platform__playcraft-project-management"
73
73
  },
74
74
  {
75
75
  "skillRef": "platform/playcraft-remix-workflow",
76
- "version": "1.0.0",
77
- "artifactHash": "07a25c34042dabbde9a166d4e49c977dd8b2459eff27c9c1bc9cafe0a56a08ef",
76
+ "version": "1.0.1",
77
+ "artifactHash": "23807ff591b4bd08c87daa4e9b2c9900b0505e4f2f601c47de51a96d6ebbed19",
78
78
  "surface": "portal-agent",
79
79
  "mountPath": ".playcraft/skills/platform__playcraft-remix-workflow"
80
80
  },
81
81
  {
82
82
  "skillRef": "platform/playcraft-save",
83
- "version": "1.0.0",
84
- "artifactHash": "100d6d66ac449bc8b7f183b8165587bcef6bb5d138d2ddfa07035f229275bc3d",
83
+ "version": "1.0.1",
84
+ "artifactHash": "6d42c681f91f849603bf3c4f235411ea05189a0d5e3ca1d3a8f92b69e536bdd1",
85
85
  "surface": "portal-agent",
86
86
  "mountPath": ".playcraft/skills/platform__playcraft-save"
87
87
  },
88
88
  {
89
89
  "skillRef": "platform/playcraft-template-upgrade-merge",
90
90
  "version": "1.0.0",
91
- "artifactHash": "459e87e02c45b5a859d1c79daf0c53cd7ee9426d6d55ef8d1156a052e557a52f",
91
+ "artifactHash": "354da60f395f3eb7a267db667783344f54567e12abcfec454b269193a5af1cb6",
92
92
  "surface": "portal-agent",
93
93
  "mountPath": ".playcraft/skills/platform__playcraft-template-upgrade-merge"
94
94
  },
95
95
  {
96
96
  "skillRef": "platform/sound-effects",
97
97
  "version": "1.0.0",
98
- "artifactHash": "3a2235197c23ca5b38c46fbb0ff1686f509c094ba206f199f78cdd5f1a07aa95",
98
+ "artifactHash": "a6ba7ea91ddbf4f5a04ae8340a69c3725c5d97b239e8636bf64886928e8dd1da",
99
99
  "surface": "portal-agent",
100
100
  "mountPath": ".playcraft/skills/platform__sound-effects"
101
101
  },
102
102
  {
103
103
  "skillRef": "remix/playcraft-adsdk",
104
104
  "version": "1.0.0",
105
- "artifactHash": "9b2e784b167372c6d3e14a3636dac2af8cf64af46c1bef1e082b77c0db51a9c2",
105
+ "artifactHash": "0de877b122246ed5c665a893f8dc408282007d0950b0f6c44876f87ceddb519f",
106
106
  "surface": "sandbox-agent",
107
107
  "mountPath": ".playcraft/skills/remix__playcraft-adsdk"
108
108
  },
109
109
  {
110
110
  "skillRef": "remix/playcraft-audio-generation",
111
111
  "version": "1.0.0",
112
- "artifactHash": "0f171e4e739d3b1a5b1f00a0b20cf13d0897c69c9d4e30dd3477acd6b982c8c8",
112
+ "artifactHash": "8d1b035b289545ca8fd6c247b9417bf34bddf5a26b31e114c35d001aa3a9ebf0",
113
113
  "surface": "sandbox-agent",
114
114
  "mountPath": ".playcraft/skills/remix__playcraft-audio-generation"
115
115
  },
116
116
  {
117
117
  "skillRef": "remix/playcraft-dist-publish",
118
118
  "version": "1.0.0",
119
- "artifactHash": "0c2396be7fed8b1a0fa2990eee2a23ed9b6abe02ead68c15648f45de7c11e254",
119
+ "artifactHash": "9a00acc9b3b6f140f276032b722bbe7bbf3a9b1fa61fe71cb65913ebcd118772",
120
120
  "surface": "sandbox-agent",
121
121
  "mountPath": ".playcraft/skills/remix__playcraft-dist-publish"
122
122
  },
123
123
  {
124
124
  "skillRef": "remix/playcraft-image-generation",
125
125
  "version": "1.0.0",
126
- "artifactHash": "a7bd5e50c62250f9c84c600239a57d71f197b88d4dedfa9eec6befc10cb4239a",
126
+ "artifactHash": "91bec2422c750d9d47d5be2796ec5aaffb48de561f2c2d8b92112f37d576f489",
127
127
  "surface": "sandbox-agent",
128
128
  "mountPath": ".playcraft/skills/remix__playcraft-image-generation"
129
129
  },
130
130
  {
131
131
  "skillRef": "remix/playcraft-image-processing",
132
132
  "version": "1.0.0",
133
- "artifactHash": "137ce0b8b76d591148dc0158b5e38abc2fc17f04471fc0d027b0a19d3dde709d",
133
+ "artifactHash": "d845c067bed103c3d1d3f145b1cb8246ffbd6a9ae91efcfbce2eae456819212d",
134
134
  "surface": "sandbox-agent",
135
135
  "mountPath": ".playcraft/skills/remix__playcraft-image-processing"
136
136
  },
137
137
  {
138
138
  "skillRef": "remix/playcraft-prefab",
139
139
  "version": "1.0.0",
140
- "artifactHash": "eb1de9851f842e475bb2ae8bb239d5f9b5ab6bd28fe0c83ae96d8570e1a8b814",
140
+ "artifactHash": "3cce28f5b67398fffaaa6295d27cf514c8e1e1e998d142524facaff7d2c25561",
141
141
  "surface": "sandbox-agent",
142
142
  "mountPath": ".playcraft/skills/remix__playcraft-prefab"
143
143
  },
144
144
  {
145
145
  "skillRef": "remix/playcraft-remix-workflow",
146
146
  "version": "1.0.0",
147
- "artifactHash": "cf5ed218579d49887d6bf2c5f4eb7512ea86684984f57546676df2d6378cd054",
147
+ "artifactHash": "e2a59c61ef5b094a821fbbab3b0f65f906bfc0dfda6f3d8c0b33a6f460dd0963",
148
148
  "surface": "sandbox-agent",
149
149
  "mountPath": ".playcraft/skills/remix__playcraft-remix-workflow"
150
150
  }