@playcraft/cli 0.1.43-beta → 0.1.44-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.
- package/dist/commands/tools-batch.js +18 -0
- package/dist/workspace-runtime/batch/control.js +80 -0
- package/dist/workspace-runtime/batch/deliver.js +150 -0
- package/dist/workspace-runtime/batch/freeze.js +102 -0
- package/dist/workspace-runtime/batch/generate.js +205 -0
- package/dist/workspace-runtime/batch/git.js +40 -0
- package/dist/workspace-runtime/batch/occupancy.js +8 -0
- package/dist/workspace-runtime/batch/policy.js +70 -0
- package/dist/workspace-runtime/batch/prepare.js +202 -0
- package/dist/workspace-runtime/batch/processes.js +72 -0
- package/dist/workspace-runtime/batch/records.js +113 -0
- package/dist/workspace-runtime/batch/routes.js +58 -0
- package/dist/workspace-runtime/batch/source-capture.js +159 -0
- package/dist/workspace-runtime/codex/app-server.js +7 -7
- package/dist/workspace-runtime/codex/permissions.js +10 -1
- package/dist/workspace-runtime/production/controller.js +3 -0
- package/dist/workspace-runtime/serve.js +2 -0
- package/dist/workspace-runtime/server/connectivity.js +10 -2
- package/dist/workspace-runtime/server/dispatch.js +4 -0
- package/dist/workspace-runtime/server/http.js +6 -0
- package/package.json +3 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
5
|
+
import { gzip, gunzip } from 'node:zlib';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { excludeFromGit } from '../workspaces/dependencies.js';
|
|
8
|
+
import { gitRead, gitWrite } from './git.js';
|
|
9
|
+
const compress = promisify(gzip);
|
|
10
|
+
const decompress = promisify(gunzip);
|
|
11
|
+
const privateNames = new Set(['.git', 'node_modules', '.ssh', '.aws', '.azure', '.gnupg', '.codex', '.npm', '.pnpm-store',
|
|
12
|
+
'.config', '.npmrc', '.yarnrc', '.yarnrc.yml', '.netrc', '.git-credentials', 'credentials.json', 'credentials', 'secrets',
|
|
13
|
+
'id_rsa', 'id_ed25519', 'settings.local.json', '.playcraft.json', 'playcraft.config.json', '.playcraft-temp', '.playcraft-preview-out']);
|
|
14
|
+
function included(path) {
|
|
15
|
+
return !path.split('/').some(name => privateNames.has(name) || /^\.env(?:$|\.)/.test(name) || /\.(?:pem|key|p12|pfx)$/.test(name))
|
|
16
|
+
&& !path.startsWith('.playcraft/skills/');
|
|
17
|
+
}
|
|
18
|
+
/** Git keeps only the executable bit, so the digest must not depend on umask or group-write bits. */
|
|
19
|
+
function snapshotMode(mode) {
|
|
20
|
+
return mode & 0o111 ? 0o755 : 0o644;
|
|
21
|
+
}
|
|
22
|
+
function inside(root, path) {
|
|
23
|
+
const rel = relative(root, path);
|
|
24
|
+
return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel);
|
|
25
|
+
}
|
|
26
|
+
export function digestFiles(files) {
|
|
27
|
+
const hash = createHash('sha256');
|
|
28
|
+
for (const [path, file] of files)
|
|
29
|
+
hash.update(path).update('\0').update(String(file.mode)).update('\0').update(file.body).update('\0');
|
|
30
|
+
return hash.digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/** Working tree plus exact skill artifacts. Credentials, caches and node_modules are left out. */
|
|
33
|
+
export async function readSourceFiles(root) {
|
|
34
|
+
const source = await realpath(root);
|
|
35
|
+
const paths = new Set((await gitRead(source, ['ls-files', '--cached', '--others', '--exclude-standard', '-z'])).split('\0').filter(Boolean));
|
|
36
|
+
const artifacts = await readdir(join(source, '.playcraft/skill-artifacts')).catch(error => {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return [];
|
|
39
|
+
throw error;
|
|
40
|
+
});
|
|
41
|
+
for (const name of artifacts)
|
|
42
|
+
if (/^[a-f0-9]{64}\.tar\.gz$/.test(name))
|
|
43
|
+
paths.add(`.playcraft/skill-artifacts/${name}`);
|
|
44
|
+
const files = new Map();
|
|
45
|
+
for (const path of [...paths].sort()) {
|
|
46
|
+
if (!included(path))
|
|
47
|
+
continue;
|
|
48
|
+
const full = join(source, path);
|
|
49
|
+
const stat = await lstat(full).catch(error => {
|
|
50
|
+
if (error.code === 'ENOENT')
|
|
51
|
+
return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
54
|
+
if (!stat)
|
|
55
|
+
continue;
|
|
56
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !inside(source, await realpath(full)))
|
|
57
|
+
throw new Error('batch_source_path_invalid');
|
|
58
|
+
const handle = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
59
|
+
try {
|
|
60
|
+
const body = await handle.readFile();
|
|
61
|
+
if (/^\.playcraft\/skill-artifacts\/[a-f0-9]{64}\.tar\.gz$/.test(path)
|
|
62
|
+
&& createHash('sha256').update(body).digest('hex') !== path.slice(path.lastIndexOf('/') + 1, -7)) {
|
|
63
|
+
throw new Error('batch_source_path_invalid');
|
|
64
|
+
}
|
|
65
|
+
files.set(path, { body, mode: snapshotMode(stat.mode) });
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await handle.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Drops paths whose bytes change between two reads. A managed writer that could not be stopped
|
|
75
|
+
* does not become part of the frozen input.
|
|
76
|
+
*/
|
|
77
|
+
export async function filterUnstableFiles(root, files) {
|
|
78
|
+
const again = await readSourceFiles(root);
|
|
79
|
+
const stable = new Map();
|
|
80
|
+
for (const [path, file] of files) {
|
|
81
|
+
const next = again.get(path);
|
|
82
|
+
if (next && next.body.equals(file.body) && next.mode === file.mode)
|
|
83
|
+
stable.set(path, file);
|
|
84
|
+
}
|
|
85
|
+
return stable;
|
|
86
|
+
}
|
|
87
|
+
/** Independent root commit. The source branch, index and config are never written. */
|
|
88
|
+
export async function captureIndependentSource(source, destination) {
|
|
89
|
+
const root = await realpath(source);
|
|
90
|
+
if (await lstat(destination).catch(() => undefined))
|
|
91
|
+
throw new Error('batch_source_destination_exists');
|
|
92
|
+
const headBefore = await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => '');
|
|
93
|
+
let files = await readSourceFiles(root);
|
|
94
|
+
files = await filterUnstableFiles(root, files);
|
|
95
|
+
const digest = digestFiles(files);
|
|
96
|
+
const staging = join(dirname(destination), `.batch-source-${randomUUID()}`);
|
|
97
|
+
try {
|
|
98
|
+
await mkdir(staging, { recursive: true });
|
|
99
|
+
await gitWrite(staging, ['init', '--initial-branch', 'source']);
|
|
100
|
+
await gitWrite(staging, ['config', 'user.name', 'PlayCraft']);
|
|
101
|
+
await gitWrite(staging, ['config', 'user.email', 'playcraft@localhost']);
|
|
102
|
+
for (const [path, file] of files) {
|
|
103
|
+
const full = join(staging, path);
|
|
104
|
+
await mkdir(dirname(full), { recursive: true });
|
|
105
|
+
await writeFile(full, file.body, { mode: file.mode, flag: 'wx' });
|
|
106
|
+
}
|
|
107
|
+
if (headBefore !== await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => ''))
|
|
108
|
+
throw new Error('batch_source_changed');
|
|
109
|
+
const after = await filterUnstableFiles(root, await readSourceFiles(root));
|
|
110
|
+
if (digestFiles(after) !== digest)
|
|
111
|
+
throw new Error('batch_source_changed');
|
|
112
|
+
await excludeFromGit(staging);
|
|
113
|
+
await excludeFromGit(staging, '/.env*');
|
|
114
|
+
for (const entry of ['/.playcraft.json', '/playcraft.config.json', '/.playcraft/skills/', '/.playcraft-temp/'])
|
|
115
|
+
await excludeFromGit(staging, entry);
|
|
116
|
+
await gitWrite(staging, ['add', '-A', '.']);
|
|
117
|
+
await gitWrite(staging, ['commit', '--allow-empty', '-m', 'Freeze batch source']);
|
|
118
|
+
const commitHash = (await gitWrite(staging, ['rev-parse', 'HEAD'])).trim();
|
|
119
|
+
await rename(staging, destination);
|
|
120
|
+
return { commitHash, digest };
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await rm(staging, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function archiveSource(root, meta) {
|
|
127
|
+
const bundle = join(dirname(root), `.bundle-${randomUUID()}`);
|
|
128
|
+
try {
|
|
129
|
+
await gitWrite(root, ['bundle', 'create', bundle, 'HEAD']);
|
|
130
|
+
const body = JSON.stringify({ version: 1, ...meta, bundle: (await readFile(bundle)).toString('base64') });
|
|
131
|
+
return await compress(Buffer.from(body));
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await rm(bundle, { force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function restoreArchive(body, destination, expectedDigest) {
|
|
138
|
+
const archive = JSON.parse((await decompress(body, { maxOutputLength: 512 * 1024 * 1024 })).toString());
|
|
139
|
+
if (archive.version !== 1 || archive.digest !== expectedDigest || !archive.commitHash || !archive.bundle)
|
|
140
|
+
throw new Error('batch_snapshot_mismatch');
|
|
141
|
+
const temporary = join(dirname(destination), `.bundle-${randomUUID()}`);
|
|
142
|
+
await mkdir(temporary, { recursive: true });
|
|
143
|
+
try {
|
|
144
|
+
const bundle = join(temporary, 'source.bundle');
|
|
145
|
+
await writeFile(bundle, Buffer.from(archive.bundle, 'base64'), { mode: 0o600 });
|
|
146
|
+
await gitWrite(temporary, ['clone', '--no-checkout', '--', bundle, destination]);
|
|
147
|
+
await gitWrite(destination, ['checkout', '--detach', archive.commitHash]);
|
|
148
|
+
if (digestFiles(await readSourceFiles(destination)) !== expectedDigest)
|
|
149
|
+
throw new Error('batch_snapshot_mismatch');
|
|
150
|
+
return { commitHash: archive.commitHash };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
await rm(destination, { recursive: true, force: true });
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await rm(temporary, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -4,7 +4,7 @@ import { startLauncherProcess } from './launcher-client.js';
|
|
|
4
4
|
import { chmod, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
5
5
|
import { relative, join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
|
-
import { APPROVAL_METHODS, executionPolicy, nativeApprovalResponse, NATIVE_APPROVALS_REVIEWER, threadExecutionParams } from './permissions.js';
|
|
7
|
+
import { APPROVAL_METHODS, executionPolicy, nativeApprovalResponse, NATIVE_APPROVALS_REVIEWER, resolveSessionExecution, sessionCanEscalate, threadExecutionParams } from './permissions.js';
|
|
8
8
|
import { workspaceApprovalResponseSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
9
|
import { portableCodexConfig, writePortableCodexCatalog } from './config-toml.js';
|
|
10
10
|
import { createJsonRpcStdioClient, NativeRpcResponseError } from './jsonrpc-stdio.js';
|
|
@@ -212,7 +212,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
212
212
|
if (await readProjectSkillsLockFile(cwd) === null) {
|
|
213
213
|
if (options.config.workspaceMode === 'multi')
|
|
214
214
|
throw new NativePreparationError('workspace_skills_unavailable');
|
|
215
|
-
const execution =
|
|
215
|
+
const execution = resolveSessionExecution(cwd, permissionMode, sessionId ? options.trustedExecution?.(sessionId) : undefined, options.fixedExecution);
|
|
216
216
|
const selected = threadExecutionParams(execution);
|
|
217
217
|
return { params: { ...modelConfig, ...selected, config: { ...modelConfig.config, ...selected.config },
|
|
218
218
|
...(options.workspaceUi && !options.fixedExecution ? { developerInstructions: WORKSPACE_UI_INSTRUCTIONS } : {}) },
|
|
@@ -239,7 +239,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
239
239
|
...(options.workspaceUi && !options.fixedExecution ? [WORKSPACE_UI_INSTRUCTIONS] : []),
|
|
240
240
|
].join('\n');
|
|
241
241
|
const discoveryHash = createHash('sha256').update(JSON.stringify(listing.skills.map(skill => skill.path).sort())).digest('hex');
|
|
242
|
-
const execution =
|
|
242
|
+
const execution = resolveSessionExecution(cwd, permissionMode, sessionId ? options.trustedExecution?.(sessionId) : undefined, options.fixedExecution);
|
|
243
243
|
const selected = threadExecutionParams(execution);
|
|
244
244
|
return { params: { ...modelConfig, ...selected, config: { ...config, ...selected.config }, developerInstructions },
|
|
245
245
|
execution, modelConfigId, activate, skillsHash: frozen.lockHash, discoveryHash, mcpHash: mcp?.hash };
|
|
@@ -678,7 +678,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
678
678
|
throw new Error('native_receipt_lost');
|
|
679
679
|
prepared.activate();
|
|
680
680
|
threads.set(threadId, { sessionId: input.sessionId, cwd: input.cwd, skillsHash: prepared.skillsHash,
|
|
681
|
-
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: prepared.execution
|
|
681
|
+
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: false, loaded: true });
|
|
682
682
|
threads.get(threadId).readOnlyConfirmed = isReadOnlySnapshot(result);
|
|
683
683
|
return { threadId, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto' };
|
|
684
684
|
});
|
|
@@ -705,7 +705,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
705
705
|
return undefined;
|
|
706
706
|
prepared.activate();
|
|
707
707
|
threads.set(input.threadId, { sessionId: input.sessionId, cwd: input.cwd, skillsHash: prepared.skillsHash,
|
|
708
|
-
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: prepared.execution
|
|
708
|
+
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: true, loaded: true });
|
|
709
709
|
threads.get(input.threadId).readOnlyConfirmed = readOnlyConfirmed;
|
|
710
710
|
return { modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto' };
|
|
711
711
|
});
|
|
@@ -730,7 +730,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
730
730
|
const permissionMode = input.permissionMode ?? thread.permissionMode ?? 'auto';
|
|
731
731
|
const workMode = input.workMode ?? 'execute';
|
|
732
732
|
let activateModel = () => { };
|
|
733
|
-
let execution =
|
|
733
|
+
let execution = resolveSessionExecution(thread.cwd ?? options.config.workspaceRoot, permissionMode, options.trustedExecution?.(input.sessionId), options.fixedExecution);
|
|
734
734
|
if (thread.cwd) {
|
|
735
735
|
try {
|
|
736
736
|
const prepared = await lockedThreadConfig(thread.cwd, input.modelConfigId ?? thread.modelConfigId, input.sessionId, reasoningEffort, input.clientRequestId, permissionMode);
|
|
@@ -774,7 +774,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
774
774
|
thread.readOnlyConfirmationFailed = false;
|
|
775
775
|
if (execution.sandboxPolicy.type !== 'readOnly' || execution.approvalPolicy !== 'never')
|
|
776
776
|
thread.readOnlyConfirmed = false;
|
|
777
|
-
thread.canEscalate =
|
|
777
|
+
thread.canEscalate = sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), execution, Boolean(options.fixedExecution));
|
|
778
778
|
if (input.simulateLostReceipt)
|
|
779
779
|
return { outcome: 'uncertain', threadId: input.threadId };
|
|
780
780
|
try {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export const NATIVE_APPROVALS_REVIEWER = 'user';
|
|
2
|
-
/**
|
|
2
|
+
/** Trusted batch policy wins over the adapter-wide fixed policy. A browser mode may only narrow it. */
|
|
3
|
+
export function resolveSessionExecution(cwd, mode, trusted, fixed) {
|
|
4
|
+
return executionPolicy(cwd, mode, trusted ?? fixed);
|
|
5
|
+
}
|
|
6
|
+
/** A trusted unattended session cannot be escalated by a browser approval. */
|
|
7
|
+
export function sessionCanEscalate(trusted, execution, adapterHasFixedExecution) {
|
|
8
|
+
if (trusted)
|
|
9
|
+
return false;
|
|
10
|
+
return execution.approvalPolicy === 'on-request' || (!adapterHasFixedExecution && execution.sandboxPolicy.type === 'dangerFullAccess');
|
|
11
|
+
}
|
|
3
12
|
export function executionPolicy(cwd, mode, fixed) {
|
|
4
13
|
const directory = fixed?.cwd ?? cwd;
|
|
5
14
|
if (mode === 'read-only')
|
|
@@ -8,6 +8,7 @@ import { productionExecutionRequestSchema } from './contracts.js';
|
|
|
8
8
|
import { executeProductionStage } from './stages.js';
|
|
9
9
|
import { groupExited, stopProductionGroup } from './processes.js';
|
|
10
10
|
import { workspaceDirectoryOccupied } from '../workspaces/directory-maintenance.js';
|
|
11
|
+
import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
11
12
|
import { settleDependencyInstalls } from '../workspaces/dependencies.js';
|
|
12
13
|
import { verifyProductionLease } from './storage.js';
|
|
13
14
|
const execute = promisify(execFile);
|
|
@@ -56,6 +57,8 @@ export async function productionDirectoryBusy(state, workspaceId, directoryName
|
|
|
56
57
|
&& !attempt.stopped && attempt.workspaceId === workspaceId && attempt.directoryName === directoryName)
|
|
57
58
|
return true;
|
|
58
59
|
}
|
|
60
|
+
if (await batchDirectoryHeld(state, workspaceId, directoryName))
|
|
61
|
+
return true;
|
|
59
62
|
return false;
|
|
60
63
|
}
|
|
61
64
|
export async function executeSandboxProduction(state, raw) {
|
|
@@ -5,6 +5,7 @@ import { createServer } from 'node:http';
|
|
|
5
5
|
import { INCOMPATIBLE_EXIT_CODE, negotiateProtocol, transitionOperation, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
6
|
import { RuntimeJournal } from './persistence/journal.js';
|
|
7
7
|
import { createCodexAppServerAdapter } from './codex/app-server.js';
|
|
8
|
+
import { trustedMemberPolicy } from './batch/policy.js';
|
|
8
9
|
import { createLoopbackNativeAdapter } from './codex/loopback.js';
|
|
9
10
|
import { attachNativeCollector } from './server/dispatch.js';
|
|
10
11
|
import { handleRuntimeRequest } from './server/http.js';
|
|
@@ -275,6 +276,7 @@ function defaultNativeAdapter(state) {
|
|
|
275
276
|
return loadWorkspaceSkillArtifact(state, workspaceId, entry, directoryName === 'root' ? 'basic' : directoryName);
|
|
276
277
|
},
|
|
277
278
|
onSkillProcessRestart: process => state.journal.noteExecutionGroup(process),
|
|
279
|
+
trustedExecution: sessionId => trustedMemberPolicy(state, sessionId),
|
|
278
280
|
});
|
|
279
281
|
}
|
|
280
282
|
return undefined;
|
|
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { PLATFORM_ROUTES, modelConnectivityPath, platformEndpoint, } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
4
|
const PROBE_TIMEOUT_MS = 4_000;
|
|
5
|
+
// Issuance performs current authority reads and signing, rather than an endpoint echo.
|
|
6
|
+
const CREDENTIAL_TIMEOUT_MS = 10_000;
|
|
5
7
|
const CACHE_MS = 15_000;
|
|
6
8
|
const CREDENTIAL_RENEW_BEFORE_MS = 30_000;
|
|
7
9
|
const MAX_RESPONSE_BYTES = 16 * 1024;
|
|
@@ -59,7 +61,7 @@ async function responseJson(response) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
async function requestProbe(target, init, credentials = false) {
|
|
62
|
-
const signal = AbortSignal.timeout(PROBE_TIMEOUT_MS);
|
|
64
|
+
const signal = AbortSignal.timeout(credentials ? CREDENTIAL_TIMEOUT_MS : PROBE_TIMEOUT_MS);
|
|
63
65
|
try {
|
|
64
66
|
const response = await fetch(target, { ...init, redirect: 'error', signal });
|
|
65
67
|
if (!response.ok) {
|
|
@@ -134,6 +136,7 @@ async function modelProxyProbe(config, entry) {
|
|
|
134
136
|
}
|
|
135
137
|
if (entry.credential && Date.parse(entry.credential.expiresAt) <= Date.now() + CREDENTIAL_RENEW_BEFORE_MS)
|
|
136
138
|
entry.credential = undefined;
|
|
139
|
+
const reusedCredential = Boolean(entry.credential);
|
|
137
140
|
if (!entry.credential) {
|
|
138
141
|
const response = await requestProbe(credentialEndpoint, {
|
|
139
142
|
method: 'POST', headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
|
|
@@ -152,8 +155,13 @@ async function modelProxyProbe(config, entry) {
|
|
|
152
155
|
const target = new URL(platformEndpoint(config.publicBaseUrl, modelConnectivityPath(credential.configId)));
|
|
153
156
|
target.searchParams.set('nonce', nonce);
|
|
154
157
|
const result = await requestProbe(target, { method: 'GET', headers: { authorization: `Bearer ${credential.token}`, 'playcraft-env': config.environment } });
|
|
155
|
-
if (result.state.reason === 'endpoint_rejected')
|
|
158
|
+
if (result.state.reason === 'endpoint_rejected') {
|
|
156
159
|
entry.credential = undefined;
|
|
160
|
+
// A restore can invalidate an otherwise unexpired credential. Reissue once for this
|
|
161
|
+
// read-only echo; a fresh refusal remains a refusal. Model turns are never replayed here.
|
|
162
|
+
if (reusedCredential)
|
|
163
|
+
return modelProxyProbe(config, entry);
|
|
164
|
+
}
|
|
157
165
|
if (result.state.status !== 'ready')
|
|
158
166
|
return result.state;
|
|
159
167
|
return echoMatches(result.body, { service: 'playcraft-model-proxy', ...scope(config), configId: credential.configId, nonce })
|
|
@@ -1241,7 +1241,11 @@ async function releaseManagedWrite(state, writeId, workspaceId) {
|
|
|
1241
1241
|
await state.journal.putManagedWrite({ ...lease, stoppedAt: new Date().toISOString() });
|
|
1242
1242
|
return true;
|
|
1243
1243
|
}
|
|
1244
|
+
const nativeCollectors = new WeakSet();
|
|
1244
1245
|
export function attachNativeCollector(state) {
|
|
1246
|
+
if (nativeCollectors.has(state))
|
|
1247
|
+
return;
|
|
1248
|
+
nativeCollectors.add(state);
|
|
1245
1249
|
state.native?.onCollected?.((event) => {
|
|
1246
1250
|
void ingestCollected(state, event).then(async () => {
|
|
1247
1251
|
if (event.kind !== 'notification' || !event.method)
|
|
@@ -25,6 +25,7 @@ import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.
|
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { terminalHost } from './terminal-host.js';
|
|
27
27
|
import { productionCapability, productionDirectoryBusy, executeSandboxProduction, sandboxProductionReceipt } from '../production/controller.js';
|
|
28
|
+
import { dispatchBatchRoute } from '../batch/routes.js';
|
|
28
29
|
import { cleanupWorkspaceDirectories, syncWorkspaceDirectories } from '../workspaces/directory-maintenance.js';
|
|
29
30
|
import { registerWorkspaceDirectory, restoreWorkspaceDirectory } from '../workspaces/directories.js';
|
|
30
31
|
import { readWorkspaceDirectoryStats } from '../workspaces/directory-stats.js';
|
|
@@ -50,6 +51,11 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
try {
|
|
54
|
+
const batch = await dispatchBatchRoute(state, request, url);
|
|
55
|
+
if (batch) {
|
|
56
|
+
writeJson(response, batch.status, batch.body);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
53
59
|
if (request.method === 'GET' && url.pathname === '/runtime/production/capability') {
|
|
54
60
|
writeJson(response, 200, await productionCapability(state));
|
|
55
61
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@gltf-transform/core": "^4.3.0",
|
|
52
52
|
"@gltf-transform/extensions": "^4.3.0",
|
|
53
53
|
"@gltf-transform/functions": "^4.3.0",
|
|
54
|
-
"@playcraft/build": "^0.1.
|
|
55
|
-
"@playcraft/common": "^0.1.
|
|
54
|
+
"@playcraft/build": "^0.1.44-beta",
|
|
55
|
+
"@playcraft/common": "^0.1.44-beta",
|
|
56
56
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
57
57
|
"chokidar": "^4.0.3",
|
|
58
58
|
"commander": "^13.1.0",
|