@ricsam/r5d-worker 0.0.141 → 0.0.142
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/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/runtime.mjs +32 -16
- package/dist/mjs/runtime/workspace/artifacts.mjs +7 -7
- package/dist/mjs/runtime/workspace/authority.mjs +190 -82
- package/dist/mjs/runtime/workspace/contracts.mjs +4 -0
- package/dist/mjs/runtime/workspace/file-write.mjs +15 -9
- package/dist/mjs/runtime/workspace/files.mjs +20 -3
- package/dist/types/personal/runtime.d.ts +18 -3
- package/dist/types/runtime/workspace/artifacts.d.ts +2 -2
- package/dist/types/runtime/workspace/authority.d.ts +16 -1
- package/dist/types/runtime/workspace/contracts.d.ts +13 -0
- package/dist/types/runtime/workspace/file-write.d.ts +1 -0
- package/dist/types/runtime/workspace/files.d.ts +3 -0
- package/package.json +3 -3
package/dist/cjs/package.json
CHANGED
package/dist/mjs/main.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { startManagerRpc } from "./runtime/releases/rpc-main.mjs";
|
|
|
7
7
|
import { ManagerRpcConfig } from "./runtime/releases/rpc-protocol.mjs";
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
if (args.includes("--version")) {
|
|
10
|
-
console.log(`r5d-worker ${true ? "0.0.
|
|
10
|
+
console.log(`r5d-worker ${true ? "0.0.142" : "development"}`);
|
|
11
11
|
} else if (!args.length || args.includes("--help")) {
|
|
12
12
|
console.log(
|
|
13
13
|
"Usage: r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <worker-token>]\n r5d-worker executor /absolute/private/config.json\n r5d-worker manager /absolute/private/config.json\n\nRun independently supervised durable runtime services. Provision configuration with r5dinfra."
|
|
@@ -15,7 +15,7 @@ if (args.includes("--version")) {
|
|
|
15
15
|
} else if (args[0] === "start") {
|
|
16
16
|
const runtime = await startPersonalWorker(
|
|
17
17
|
parsePersonalWorkerOptions(args.slice(1)),
|
|
18
|
-
true ? "0.0.
|
|
18
|
+
true ? "0.0.142" : "development"
|
|
19
19
|
);
|
|
20
20
|
console.log(`Worker connected: ${runtime.resourceId}`);
|
|
21
21
|
let closing = false;
|
package/dist/mjs/package.json
CHANGED
|
@@ -23,7 +23,15 @@ const PersonalWorkerGrant = z.object({
|
|
|
23
23
|
workerHello: RuntimeHello,
|
|
24
24
|
leaseExpiresAt: z.coerce.number().int().positive()
|
|
25
25
|
}).passthrough();
|
|
26
|
-
const Workbench = z.object({
|
|
26
|
+
const Workbench = z.object({
|
|
27
|
+
id: StorageId,
|
|
28
|
+
repositoryId: StorageId,
|
|
29
|
+
branch: BranchName,
|
|
30
|
+
sessionId: RuntimeId,
|
|
31
|
+
rootProfile: z.enum(["account", "project"]).default("project"),
|
|
32
|
+
namespace: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/).optional(),
|
|
33
|
+
projectName: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/).optional()
|
|
34
|
+
}).strict();
|
|
27
35
|
const PersonalWorkspaceRequest = z.object({
|
|
28
36
|
protocol: z.literal(1),
|
|
29
37
|
userId: RuntimeId,
|
|
@@ -67,9 +75,10 @@ async function openPersonalWorkerRuntime(options) {
|
|
|
67
75
|
privateDirectory(root);
|
|
68
76
|
if (grant.workerFence.installationId !== grant.installationId || grant.workerHello.installationId !== grant.installationId || grant.workerHello.instanceId !== grant.workerFence.ownerId || grant.workerHello.role !== "worker-adapter")
|
|
69
77
|
throw new Error("Invalid personal worker grant");
|
|
70
|
-
const credentialsRoot = path.join(root, "credentials"), workspaceRoot = path.join(root, "workspace", grant.installationId);
|
|
78
|
+
const credentialsRoot = path.join(root, "credentials"), workspaceRoot = path.join(root, "workspace", grant.installationId), authorityRoot = path.join(root, "workspace-authority", grant.installationId), hostRoot = path.parse(workspaceRoot).root;
|
|
71
79
|
privateDirectory(credentialsRoot);
|
|
72
80
|
privateDirectory(workspaceRoot);
|
|
81
|
+
privateDirectory(authorityRoot);
|
|
73
82
|
const token = await personalExecutorToken(root, grant), controllerId = `controller-${createHash("sha256").update(grant.instanceId).digest("hex").slice(0, 24)}`;
|
|
74
83
|
const daemon = await startHostExecutor({
|
|
75
84
|
installationId: grant.installationId,
|
|
@@ -86,7 +95,7 @@ async function openPersonalWorkerRuntime(options) {
|
|
|
86
95
|
ownerId: controllerId,
|
|
87
96
|
tokenHash: tokenHash(token),
|
|
88
97
|
userIds: [grant.userId],
|
|
89
|
-
cwdRoots: [
|
|
98
|
+
cwdRoots: [hostRoot],
|
|
90
99
|
lanes: ["general", "utility", "control"],
|
|
91
100
|
credentialIds: []
|
|
92
101
|
}
|
|
@@ -123,13 +132,14 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
123
132
|
timeoutMs: 1e4
|
|
124
133
|
});
|
|
125
134
|
let current = grant, installed = false, retirementPending = false;
|
|
126
|
-
const manifests = /* @__PURE__ */ new Map();
|
|
135
|
+
const manifests = /* @__PURE__ */ new Map(), routes = /* @__PURE__ */ new Map();
|
|
127
136
|
const manifestFile = path.join(root, "workbenches.json");
|
|
128
137
|
try {
|
|
129
138
|
const rows = z.array(ApprovedWorkbench).parse(readPrivateJson(manifestFile));
|
|
130
139
|
for (const row of rows) {
|
|
131
|
-
if (row.userId !== grant.userId || !row.cwd.startsWith(workspaceRoot + path.sep))
|
|
132
|
-
|
|
140
|
+
if (row.userId !== grant.userId || row.cwd !== workspaceRoot && !row.cwd.startsWith(workspaceRoot + path.sep))
|
|
141
|
+
throw new Error("Invalid retained workbench");
|
|
142
|
+
manifests.set(row.id, row);
|
|
133
143
|
}
|
|
134
144
|
} catch (error) {
|
|
135
145
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -160,7 +170,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
160
170
|
ownerId: next.workerHello.instanceId,
|
|
161
171
|
tokenHash: tokenHash(token),
|
|
162
172
|
userIds: [grant.userId],
|
|
163
|
-
cwdRoots: [
|
|
173
|
+
cwdRoots: [hostRoot],
|
|
164
174
|
lanes: ["general", "utility", "control"],
|
|
165
175
|
credentialIds: []
|
|
166
176
|
}
|
|
@@ -185,22 +195,27 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
185
195
|
}
|
|
186
196
|
await renew(grant);
|
|
187
197
|
const authority = await WorkspaceAuthority.open({
|
|
188
|
-
config: { installationId: grant.installationId, root: workspaceRoot, dynamicWorkbenches: true, workbenches: [] },
|
|
198
|
+
config: { installationId: grant.installationId, root: authorityRoot, workspaceRoot, dynamicWorkbenches: true, workbenches: [] },
|
|
189
199
|
executor: async () => ({ client, workerFence: current.workerFence }),
|
|
190
|
-
resolveWorkbench: async (identity) => identity.userId === grant.userId ? manifests.get(identity.sessionId) ?? null : null,
|
|
200
|
+
resolveWorkbench: async (identity) => identity.userId === grant.userId ? manifests.get(routes.get(identity.sessionId) ?? "") ?? null : null,
|
|
191
201
|
storage: async (identity) => new WorkspaceStorageClient(grant.installationId, options.storage(identity.sessionId), async () => current.workerFence)
|
|
192
202
|
});
|
|
193
203
|
async function approve(input) {
|
|
204
|
+
const cwd = input.rootProfile === "account" ? workspaceRoot : input.namespace && input.projectName ? path.join(workspaceRoot, "projects", input.namespace, input.projectName, ...input.branch.split("/")) : path.join(workspaceRoot, "workbenches", input.id);
|
|
194
205
|
const row = ApprovedWorkbench.parse({
|
|
195
206
|
...input,
|
|
196
207
|
userId: grant.userId,
|
|
197
|
-
cwd
|
|
208
|
+
cwd,
|
|
198
209
|
nonmutatingArgv: []
|
|
199
210
|
});
|
|
200
|
-
const previous = manifests.get(input.
|
|
201
|
-
|
|
211
|
+
const previous = manifests.get(input.id);
|
|
212
|
+
const stable = (value) => {
|
|
213
|
+
const { sessionId: _sessionId, sharedSessionId: _sharedSessionId, ...binding } = value;
|
|
214
|
+
return binding;
|
|
215
|
+
};
|
|
216
|
+
if (previous && canonicalJson(stable(previous)) !== canonicalJson(stable(row))) throw new Error("Personal workbench identity changed");
|
|
202
217
|
if (!previous) {
|
|
203
|
-
manifests.set(input.
|
|
218
|
+
manifests.set(input.id, row);
|
|
204
219
|
const tmp = `${manifestFile}.${randomBytes(8).toString("hex")}.next`;
|
|
205
220
|
const handle = await fs.open(tmp, "wx", 384);
|
|
206
221
|
try {
|
|
@@ -211,6 +226,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
211
226
|
}
|
|
212
227
|
await fs.rename(tmp, manifestFile);
|
|
213
228
|
}
|
|
229
|
+
routes.set(input.sessionId, input.id);
|
|
214
230
|
}
|
|
215
231
|
async function installCredentials(values) {
|
|
216
232
|
for (const source of values) {
|
|
@@ -234,7 +250,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
234
250
|
const tcp = new PersonalTcpManager(path.join(root, "tcp"));
|
|
235
251
|
async function dispatch(raw) {
|
|
236
252
|
const request = PersonalWorkspaceRequest.parse(raw);
|
|
237
|
-
if (request.userId !== grant.userId
|
|
253
|
+
if (request.userId !== grant.userId)
|
|
238
254
|
throw new Error("Wrong personal worker principal");
|
|
239
255
|
if (request.kind === "tcp") return tcp.dispatch(request.sessionId, request.command);
|
|
240
256
|
for (const parent of request.sharedWorkbenches ?? []) await approve(parent);
|
|
@@ -268,7 +284,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
268
284
|
fence: current.workerFence,
|
|
269
285
|
payload: {
|
|
270
286
|
argv,
|
|
271
|
-
cwd: manifests.get(identity.sessionId).cwd,
|
|
287
|
+
cwd: manifests.get(routes.get(identity.sessionId)).cwd,
|
|
272
288
|
...command.method === "open" ? { interactive: true, pty: { cols: command.cols, rows: command.rows } } : {},
|
|
273
289
|
credentials: (request.credentials ?? []).map(({ id, generation }) => ({ id, generation }))
|
|
274
290
|
}
|
|
@@ -321,7 +337,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
321
337
|
fence: current.workerFence,
|
|
322
338
|
payload: {
|
|
323
339
|
...operation.payload,
|
|
324
|
-
cwd: manifests.get(identity.sessionId).cwd,
|
|
340
|
+
cwd: manifests.get(routes.get(identity.sessionId)).cwd,
|
|
325
341
|
credentials: (request.credentials ?? []).map(({ id, generation }) => ({ id, generation }))
|
|
326
342
|
}
|
|
327
343
|
});
|
|
@@ -19,19 +19,19 @@ const SessionArtifactChunk = z.object({
|
|
|
19
19
|
const RESERVED_ARTIFACT_ENV = ["R5D_ROOT", "R5D_SESSION_ID"];
|
|
20
20
|
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
21
21
|
class SessionArtifactStore {
|
|
22
|
-
constructor(
|
|
23
|
-
this.
|
|
22
|
+
constructor(workspaceRoot) {
|
|
23
|
+
this.workspaceRoot = workspaceRoot;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
workspaceRoot;
|
|
26
26
|
async environment(identity) {
|
|
27
27
|
const scope = { userId: RuntimeId.parse(identity.userId), sessionId: RuntimeId.parse(identity.sessionId) };
|
|
28
|
-
const
|
|
28
|
+
const root = this.workspaceRoot, incoming = path.join(root, ".incoming", sha(canonicalJson(scope)));
|
|
29
29
|
for (const directory of [
|
|
30
|
-
base,
|
|
31
30
|
root,
|
|
32
31
|
path.join(root, "artifacts"),
|
|
33
32
|
path.join(root, "artifacts", identity.sessionId),
|
|
34
|
-
path.join(root, ".incoming")
|
|
33
|
+
path.join(root, ".incoming"),
|
|
34
|
+
incoming
|
|
35
35
|
]) {
|
|
36
36
|
await fs.mkdir(directory, { mode: 448 }).catch((error) => {
|
|
37
37
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -44,7 +44,7 @@ class SessionArtifactStore {
|
|
|
44
44
|
const chunk = SessionArtifactChunk.parse(input), bytes = Buffer.from(chunk.base64, "base64");
|
|
45
45
|
if (bytes.length > 64 * 1024 || bytes.toString("base64") !== chunk.base64 || chunk.offset + bytes.length > chunk.size)
|
|
46
46
|
throw new WorkspaceError("invalid_artifact_chunk", "Invalid bounded artifact bytes");
|
|
47
|
-
const env = await this.environment(identity), destination = path.join(env.R5D_ROOT, "artifacts", identity.sessionId, chunk.filename), incoming = path.join(env.R5D_ROOT, ".incoming", sha(chunk.filename));
|
|
47
|
+
const env = await this.environment(identity), destination = path.join(env.R5D_ROOT, "artifacts", identity.sessionId, chunk.filename), incoming = path.join(env.R5D_ROOT, ".incoming", sha(canonicalJson({ userId: identity.userId, sessionId: identity.sessionId })), sha(chunk.filename));
|
|
48
48
|
const binding = { filename: chunk.filename, sha256: chunk.sha256, size: chunk.size }, manifest = `${incoming}.json`, temporary = `${incoming}.part`;
|
|
49
49
|
const old = await readRegular(manifest, 1024).catch((error) => {
|
|
50
50
|
if (error.code === "ENOENT") return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { canonicalJson, OperationEnvelope, operationPayloadHash } from "@ricsam/r5d-api/runtime-protocol";
|
|
6
7
|
import { STORAGE_LIMITS, GitOid, BranchName, safeTreePath } from "./storage-wire.mjs";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
git,
|
|
22
23
|
noSymlinkAncestors,
|
|
23
24
|
privateRoot,
|
|
25
|
+
readHostRegular,
|
|
24
26
|
readRegular,
|
|
25
27
|
selectedTree,
|
|
26
28
|
sha256,
|
|
@@ -42,7 +44,61 @@ class WorkspaceAuthority {
|
|
|
42
44
|
closed = false;
|
|
43
45
|
closeTask;
|
|
44
46
|
actions = /* @__PURE__ */ new Set();
|
|
47
|
+
repositoryInitializations = /* @__PURE__ */ new Map();
|
|
45
48
|
pending = 0;
|
|
49
|
+
expectedCwd(config) {
|
|
50
|
+
const workspaceRoot = this.config.workspaceRoot ?? this.config.root;
|
|
51
|
+
if (config.rootProfile === "account") return workspaceRoot;
|
|
52
|
+
if (config.namespace && config.projectName)
|
|
53
|
+
return path.join(workspaceRoot, "projects", config.namespace, config.projectName, ...config.branch.split("/"));
|
|
54
|
+
return path.join(workspaceRoot, "workbenches", config.id);
|
|
55
|
+
}
|
|
56
|
+
stableBinding(config) {
|
|
57
|
+
const { sessionId: _sessionId, sharedSessionId: _sharedSessionId, ...binding } = config;
|
|
58
|
+
return binding;
|
|
59
|
+
}
|
|
60
|
+
repositoryPath(config, directory) {
|
|
61
|
+
return config.namespace && config.projectName ? path.join(this.config.root, "repositories", config.repositoryId) : path.join(directory, "repo");
|
|
62
|
+
}
|
|
63
|
+
async ensureRepository(config, directory) {
|
|
64
|
+
const repo = this.repositoryPath(config, directory);
|
|
65
|
+
let task = this.repositoryInitializations.get(repo);
|
|
66
|
+
if (!task) {
|
|
67
|
+
task = (async () => {
|
|
68
|
+
const exists = await fs.lstat(repo).then(() => true, (error) => {
|
|
69
|
+
if (error.code === "ENOENT") return false;
|
|
70
|
+
throw error;
|
|
71
|
+
});
|
|
72
|
+
if (!exists) {
|
|
73
|
+
await fs.mkdir(repo, { recursive: true, mode: 448 });
|
|
74
|
+
await git(repo, ["init", "--bare", "--template=", "."]);
|
|
75
|
+
}
|
|
76
|
+
await ensureAuthorityGitRepositoryLayout(repo);
|
|
77
|
+
return repo;
|
|
78
|
+
})();
|
|
79
|
+
this.repositoryInitializations.set(repo, task);
|
|
80
|
+
void task.finally(() => this.repositoryInitializations.delete(repo)).catch(() => {
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return task;
|
|
84
|
+
}
|
|
85
|
+
assertNonOverlappingCheckout(config) {
|
|
86
|
+
if (config.rootProfile !== "project") return;
|
|
87
|
+
for (const bench of this.benches.values()) {
|
|
88
|
+
if (bench.config.rootProfile !== "project" || bench.config.id === config.id || bench.state.blocked?.code === "archived") continue;
|
|
89
|
+
const relative = path.relative(bench.config.cwd, config.cwd), reverse = path.relative(config.cwd, bench.config.cwd);
|
|
90
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative) || !reverse.startsWith("..") && !path.isAbsolute(reverse))
|
|
91
|
+
throw new WorkspaceError("overlapping_branch_path", "Branch worktree paths may not contain one another");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
linkedWorkbench(b) {
|
|
95
|
+
return Boolean(b.config.namespace && b.config.projectName);
|
|
96
|
+
}
|
|
97
|
+
resolveHostPath(value) {
|
|
98
|
+
const expanded = value === "~" ? os.homedir() : value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
99
|
+
if (!path.isAbsolute(expanded) || expanded.includes("\0")) throw new WorkspaceError("unsafe_path", "Host path must be absolute or home-relative");
|
|
100
|
+
return path.normalize(expanded);
|
|
101
|
+
}
|
|
46
102
|
static async open(options) {
|
|
47
103
|
const authority = new WorkspaceAuthority(options);
|
|
48
104
|
const { config } = authority;
|
|
@@ -52,10 +108,8 @@ class WorkspaceAuthority {
|
|
|
52
108
|
if (Boolean(config.dynamicWorkbenches) !== Boolean(options.resolveWorkbench)) throw new WorkspaceError("invalid_config", "Dynamic workspace requires catalog authorization");
|
|
53
109
|
if (new Set(config.workbenches.map((b) => b.id)).size !== config.workbenches.length || new Set(config.workbenches.map((b) => `${b.userId}:${b.sessionId}`)).size !== config.workbenches.length)
|
|
54
110
|
throw new WorkspaceError("invalid_config", "Duplicate workbench or session route");
|
|
55
|
-
for (const b of config.workbenches)
|
|
56
|
-
if (b.cwd !==
|
|
57
|
-
throw new WorkspaceError("unsafe_root", "Approved cwd must equal root/workbenches/id; arbitrary and legacy roots are forbidden");
|
|
58
|
-
}
|
|
111
|
+
for (const b of config.workbenches)
|
|
112
|
+
if (b.cwd !== authority.expectedCwd(b)) throw new WorkspaceError("unsafe_root", "Approved cwd does not match its managed worktree path");
|
|
59
113
|
if (contents.length) {
|
|
60
114
|
const existing = JSON.parse((await readRegular(path.join(config.root, "installation.json"), 128 * 1024)).toString());
|
|
61
115
|
if (canonicalJson(existing) !== canonicalJson(binding))
|
|
@@ -72,13 +126,13 @@ class WorkspaceAuthority {
|
|
|
72
126
|
await durableJson(path.join(config.root, "installation.json"), binding);
|
|
73
127
|
await fs.mkdir(path.join(config.root, "workbenches"), { mode: 448 });
|
|
74
128
|
await fs.mkdir(path.join(config.root, "state"), { mode: 448 });
|
|
129
|
+
await fs.mkdir(path.join(config.root, "repositories"), { mode: 448 });
|
|
75
130
|
for (const b of config.workbenches) {
|
|
76
|
-
await fs.mkdir(b.cwd, { mode: 448 });
|
|
131
|
+
await fs.mkdir(b.cwd, { recursive: true, mode: 448 });
|
|
77
132
|
const directory = path.join(config.root, "state", b.id);
|
|
78
133
|
await fs.mkdir(directory, { mode: 448 });
|
|
79
|
-
await
|
|
80
|
-
await
|
|
81
|
-
await durableJson(path.join(directory, "state.json"), { initialized: false, head: null, blocked: null, runs: {} });
|
|
134
|
+
await authority.ensureRepository(b, directory);
|
|
135
|
+
await durableJson(path.join(directory, "state.json"), { initialized: b.rootProfile === "account", head: null, blocked: null, runs: {} });
|
|
82
136
|
}
|
|
83
137
|
}
|
|
84
138
|
for (const b of config.workbenches) {
|
|
@@ -87,8 +141,7 @@ class WorkspaceAuthority {
|
|
|
87
141
|
const state = JSON.parse((await readRegular(path.join(directory, "state.json"), 1024 * 1024)).toString());
|
|
88
142
|
if (typeof state.initialized !== "boolean" || !state.runs || !(state.head === null || GitOid.safeParse(state.head).success))
|
|
89
143
|
throw new WorkspaceError("invalid_state", "Invalid durable workspace state; maintenance required");
|
|
90
|
-
const repo =
|
|
91
|
-
await ensureAuthorityGitRepositoryLayout(repo);
|
|
144
|
+
const repo = await authority.ensureRepository(b, directory);
|
|
92
145
|
authority.benches.set(b.id, { config: b, state, directory, repo });
|
|
93
146
|
}
|
|
94
147
|
if (config.dynamicWorkbenches) {
|
|
@@ -97,12 +150,11 @@ class WorkspaceAuthority {
|
|
|
97
150
|
const directory = path.join(config.root, "state", id);
|
|
98
151
|
await noSymlinkAncestors(directory);
|
|
99
152
|
const b = ApprovedWorkbench.parse(JSON.parse((await readRegular(path.join(directory, "workbench.json"), 128 * 1024)).toString()));
|
|
100
|
-
if (b.id !== id || b.cwd !==
|
|
153
|
+
if (b.id !== id || b.cwd !== authority.expectedCwd(b)) throw new WorkspaceError("wrong_binding", "Invalid durable dynamic workbench");
|
|
101
154
|
await noSymlinkAncestors(b.cwd);
|
|
102
155
|
const state = JSON.parse((await readRegular(path.join(directory, "state.json"), 1024 * 1024)).toString());
|
|
103
156
|
if (typeof state.initialized !== "boolean" || !state.runs || !(state.head === null || GitOid.safeParse(state.head).success)) throw new WorkspaceError("invalid_state", "Invalid dynamic workspace state");
|
|
104
|
-
const repo =
|
|
105
|
-
await ensureAuthorityGitRepositoryLayout(repo);
|
|
157
|
+
const repo = await authority.ensureRepository(b, directory);
|
|
106
158
|
authority.benches.set(id, { config: b, state, directory, repo });
|
|
107
159
|
}
|
|
108
160
|
}
|
|
@@ -115,7 +167,7 @@ class WorkspaceAuthority {
|
|
|
115
167
|
const approved = fixed ?? await this.options.resolveWorkbench?.(identity);
|
|
116
168
|
if (!approved) throw new WorkspaceError("forbidden", "No approved workbench for authenticated user/session");
|
|
117
169
|
const config = ApprovedWorkbench.parse(approved);
|
|
118
|
-
if (config.userId !== identity.userId || config.
|
|
170
|
+
if (config.userId !== identity.userId || config.cwd !== this.expectedCwd(config))
|
|
119
171
|
throw new WorkspaceError("unsafe_root", "Catalog returned an invalid workspace identity");
|
|
120
172
|
if (config.sharedSessionId) {
|
|
121
173
|
const parent = await this.bench({ userId: identity.userId, sessionId: config.sharedSessionId }, /* @__PURE__ */ new Set([...visited, identity.sessionId]));
|
|
@@ -124,9 +176,11 @@ class WorkspaceAuthority {
|
|
|
124
176
|
}
|
|
125
177
|
const existing = this.benches.get(config.id);
|
|
126
178
|
if (existing) {
|
|
127
|
-
if (canonicalJson(existing.config) !== canonicalJson(config))
|
|
179
|
+
if (canonicalJson(this.stableBinding(existing.config)) !== canonicalJson(this.stableBinding(config)))
|
|
180
|
+
throw new WorkspaceError("wrong_binding", "Workbench identity is immutable");
|
|
128
181
|
return existing;
|
|
129
182
|
}
|
|
183
|
+
this.assertNonOverlappingCheckout(config);
|
|
130
184
|
let task = this.registrations.get(config.id);
|
|
131
185
|
if (!task) {
|
|
132
186
|
task = (async () => {
|
|
@@ -138,20 +192,20 @@ class WorkspaceAuthority {
|
|
|
138
192
|
throw error;
|
|
139
193
|
});
|
|
140
194
|
if (stored) {
|
|
141
|
-
|
|
195
|
+
const persisted = ApprovedWorkbench.parse(JSON.parse(stored));
|
|
196
|
+
if (canonicalJson(this.stableBinding(persisted)) !== canonicalJson(this.stableBinding(config)))
|
|
197
|
+
throw new WorkspaceError("wrong_binding", "Persisted workbench identity changed");
|
|
142
198
|
} else {
|
|
143
199
|
await fs.mkdir(directory, { mode: 448 });
|
|
144
200
|
await durableJson(manifest, config);
|
|
145
|
-
await fs.mkdir(config.cwd, { mode: 448 });
|
|
146
|
-
await
|
|
147
|
-
await
|
|
148
|
-
await durableJson(path.join(directory, "state.json"), { initialized: false, head: null, blocked: null, runs: {} });
|
|
201
|
+
await fs.mkdir(config.cwd, { recursive: true, mode: 448 });
|
|
202
|
+
await this.ensureRepository(config, directory);
|
|
203
|
+
await durableJson(path.join(directory, "state.json"), { initialized: config.rootProfile === "account", head: null, blocked: null, runs: {} });
|
|
149
204
|
}
|
|
150
205
|
await noSymlinkAncestors(config.cwd);
|
|
151
206
|
const state = JSON.parse((await readRegular(path.join(directory, "state.json"), 1024 * 1024)).toString());
|
|
152
207
|
if (typeof state.initialized !== "boolean" || !state.runs || !(state.head === null || GitOid.safeParse(state.head).success)) throw new WorkspaceError("invalid_state", "Invalid workspace state");
|
|
153
|
-
const repo =
|
|
154
|
-
await ensureAuthorityGitRepositoryLayout(repo);
|
|
208
|
+
const repo = await this.ensureRepository(config, directory);
|
|
155
209
|
const bench = { config, state, directory, repo };
|
|
156
210
|
this.benches.set(config.id, bench);
|
|
157
211
|
return bench;
|
|
@@ -165,6 +219,7 @@ class WorkspaceAuthority {
|
|
|
165
219
|
hydrations = /* @__PURE__ */ new Map();
|
|
166
220
|
async ensureHydrated(identity) {
|
|
167
221
|
const b = await this.bench(identity);
|
|
222
|
+
if (b.config.rootProfile === "account") return;
|
|
168
223
|
if (b.state.initialized) return;
|
|
169
224
|
let task = this.hydrations.get(b.config.id);
|
|
170
225
|
if (!task) {
|
|
@@ -186,7 +241,7 @@ class WorkspaceAuthority {
|
|
|
186
241
|
return { path: file, content: bytes.toString("utf8"), bytes: bytes.length };
|
|
187
242
|
}
|
|
188
243
|
async processes(identity) {
|
|
189
|
-
const b = await this.bench(identity), route = await this.route(b);
|
|
244
|
+
const b = await this.bench(identity), route = await this.route(b, identity);
|
|
190
245
|
const result = [];
|
|
191
246
|
for (const [operationId, metadata] of Object.entries(b.state.runs)) {
|
|
192
247
|
if (metadata.state === "rejected_capacity" || (metadata.sessionId ?? b.config.sessionId) !== identity.sessionId) continue;
|
|
@@ -214,18 +269,22 @@ class WorkspaceAuthority {
|
|
|
214
269
|
/** Bounded file browsing through the same catalog-authorized workbench as tools. */
|
|
215
270
|
async inspectFiles(identity, command) {
|
|
216
271
|
const b = await this.bench(identity);
|
|
217
|
-
const relative = command.path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
218
|
-
if (relative) safeTreePath(relative);
|
|
219
|
-
if (!relative && command.method === "raw") throw new WorkspaceError("unsafe_path", "A file path is required");
|
|
220
|
-
const target = path.join(b.config.cwd, relative);
|
|
221
|
-
|
|
272
|
+
const relative = command.hostPath ? "" : command.path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
273
|
+
if (!command.hostPath && relative) safeTreePath(relative);
|
|
274
|
+
if (!command.hostPath && !relative && command.method === "raw") throw new WorkspaceError("unsafe_path", "A file path is required");
|
|
275
|
+
const target = command.hostPath ? this.resolveHostPath(command.path) : path.join(b.config.cwd, relative);
|
|
276
|
+
const display = command.hostPath ? target : `/${relative}`;
|
|
277
|
+
const inspect = command.hostPath ? fs.lstat(target).then((stat2) => {
|
|
278
|
+
if (stat2.isSymbolicLink()) throw new WorkspaceError("unsafe_path", "Final symlink paths are not inspected");
|
|
279
|
+
}) : noSymlinkAncestors(target);
|
|
280
|
+
await inspect.catch((error) => {
|
|
222
281
|
if (error.code === "ENOENT" || error.code === "ENOTDIR") throw new WorkspaceError("not_found", "File or directory not found");
|
|
223
282
|
throw error;
|
|
224
283
|
});
|
|
225
284
|
const stat = await fs.stat(target);
|
|
226
285
|
if (command.method === "raw") {
|
|
227
|
-
const bytes = await readRegular(target, 8 * 1024 * 1024);
|
|
228
|
-
return { path:
|
|
286
|
+
const bytes = await (command.hostPath ? readHostRegular : readRegular)(target, 8 * 1024 * 1024);
|
|
287
|
+
return { path: display, base64: bytes.toString("base64"), size: bytes.length, modifiedAt: stat.mtime.toISOString() };
|
|
229
288
|
}
|
|
230
289
|
if (!stat.isDirectory()) throw new WorkspaceError("not_found", "Directory not found");
|
|
231
290
|
const names = await fs.readdir(target, { withFileTypes: true });
|
|
@@ -234,13 +293,13 @@ class WorkspaceAuthority {
|
|
|
234
293
|
const nested = await fs.stat(path.join(target, entry.name));
|
|
235
294
|
return {
|
|
236
295
|
name: entry.name,
|
|
237
|
-
path: `/${path.posix.join(relative, entry.name)}`,
|
|
296
|
+
path: command.hostPath ? path.join(target, entry.name) : `/${path.posix.join(relative, entry.name)}`,
|
|
238
297
|
type: entry.isDirectory() ? "directory" : "file",
|
|
239
298
|
size: entry.isDirectory() ? null : nested.size,
|
|
240
299
|
modifiedAt: nested.mtime.toISOString()
|
|
241
300
|
};
|
|
242
301
|
}));
|
|
243
|
-
return { directory:
|
|
302
|
+
return { directory: display, entries };
|
|
244
303
|
}
|
|
245
304
|
async inspectGit(identity, command) {
|
|
246
305
|
const b = await this.bench(identity);
|
|
@@ -299,15 +358,16 @@ class WorkspaceAuthority {
|
|
|
299
358
|
if (this.closing && !admitted) return Promise.reject(new WorkspaceError("authority_closed", "Workspace authority is closing"));
|
|
300
359
|
if (this.pending >= 32) return Promise.reject(new WorkspaceError("busy", "Workspace request budget reached"));
|
|
301
360
|
this.pending++;
|
|
302
|
-
const
|
|
361
|
+
const queueKey = this.linkedWorkbench(b) ? b.repo : b.config.id;
|
|
362
|
+
const task = (this.queues.get(queueKey) ?? Promise.resolve()).catch(() => {
|
|
303
363
|
}).then(async () => {
|
|
304
364
|
await this.owned(admitted);
|
|
305
365
|
return fn();
|
|
306
366
|
});
|
|
307
|
-
this.queues.set(
|
|
367
|
+
this.queues.set(queueKey, task);
|
|
308
368
|
void task.finally(() => {
|
|
309
369
|
this.pending--;
|
|
310
|
-
if (this.queues.get(
|
|
370
|
+
if (this.queues.get(queueKey) === task) this.queues.delete(queueKey);
|
|
311
371
|
}).catch(() => {
|
|
312
372
|
});
|
|
313
373
|
return task;
|
|
@@ -318,8 +378,8 @@ class WorkspaceAuthority {
|
|
|
318
378
|
throw new WorkspaceError("wrong_installation", "Executor route is for another installation");
|
|
319
379
|
return route;
|
|
320
380
|
}
|
|
321
|
-
async storage(b) {
|
|
322
|
-
const storage = await this.options.storage(
|
|
381
|
+
async storage(b, identity) {
|
|
382
|
+
const storage = await this.options.storage(identity);
|
|
323
383
|
if (storage.installationId !== this.config.installationId)
|
|
324
384
|
throw new WorkspaceError("wrong_installation", "Storage client is for another installation");
|
|
325
385
|
return storage;
|
|
@@ -365,7 +425,7 @@ class WorkspaceAuthority {
|
|
|
365
425
|
} catch {
|
|
366
426
|
}
|
|
367
427
|
}
|
|
368
|
-
return { workbench: b.config, ...structuredClone(b.state), files };
|
|
428
|
+
return { workbench: { ...b.config, sessionId: identity.sessionId }, ...structuredClone(b.state), files };
|
|
369
429
|
});
|
|
370
430
|
}
|
|
371
431
|
async cleanRefreshBase(b, expectedBase) {
|
|
@@ -374,6 +434,12 @@ class WorkspaceAuthority {
|
|
|
374
434
|
const expectedTree = (await git(b.repo, ["rev-parse", `${expectedBase}^{tree}`])).toString().trim();
|
|
375
435
|
if (await snapshotTree(b.repo, b.config.cwd, expectedBase) !== expectedTree)
|
|
376
436
|
throw new WorkspaceError("dirty_workbench", "Source differs from its canonical base; preserve and publish or reconcile edits first");
|
|
437
|
+
if (this.linkedWorkbench(b)) {
|
|
438
|
+
const localHead2 = (await git(b.config.cwd, ["rev-parse", "HEAD"])).toString().trim();
|
|
439
|
+
if (localHead2 !== expectedBase || (await git(b.config.cwd, ["write-tree"])).toString().trim() !== expectedTree)
|
|
440
|
+
throw new WorkspaceError("dirty_workbench", "Local branch or index changed; preserve and reconcile it before refreshing");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
377
443
|
const metadata = path.join(b.config.cwd, ".git");
|
|
378
444
|
const ref = `refs/heads/${b.config.branch}`;
|
|
379
445
|
const indexBytes = await readRegular(path.join(metadata, "index"), 8 * 1024 * 1024);
|
|
@@ -392,15 +458,16 @@ class WorkspaceAuthority {
|
|
|
392
458
|
* idle workbench refresh; all replaced source/index/ref bytes are retained. */
|
|
393
459
|
async hydrate(identity, expectedBase) {
|
|
394
460
|
const b = await this.bench(identity);
|
|
395
|
-
|
|
461
|
+
if (b.config.rootProfile === "account") return { head: "", unchanged: true };
|
|
462
|
+
return this.serial(b, () => this.hydrateIdle(b, identity, expectedBase));
|
|
396
463
|
}
|
|
397
|
-
async hydrateIdle(b, expectedBase) {
|
|
464
|
+
async hydrateIdle(b, identity, expectedBase) {
|
|
398
465
|
if (b.state.blocked?.code === "account_initialization_unknown") {
|
|
399
466
|
await this.idle(b, true);
|
|
400
467
|
await this.assertFreshAccountWorkbench(b, expectedBase);
|
|
401
468
|
const operationId = `account-initialize-${sha256(b.config.userId)}`;
|
|
402
469
|
if (b.state.blocked.operationId !== operationId) throw new WorkspaceError("wrong_binding", "Account initialization receipt identity changed");
|
|
403
|
-
const storage2 = await this.storage(b);
|
|
470
|
+
const storage2 = await this.storage(b, identity);
|
|
404
471
|
const receipt = await storage2.read({ method: "operation.get", lookupId: operationId });
|
|
405
472
|
if (receipt.state !== "completed" || receipt.result?.repositoryId !== b.config.repositoryId || !GitOid.safeParse(receipt.result?.head).success)
|
|
406
473
|
throw new WorkspaceError("account_initialization_unknown", "Original account initialization is not proven complete; preserve its receipt");
|
|
@@ -416,7 +483,7 @@ class WorkspaceAuthority {
|
|
|
416
483
|
"nonempty_workbench",
|
|
417
484
|
"Hydration requires initially empty approved workbench; preserve/import existing work explicitly"
|
|
418
485
|
);
|
|
419
|
-
const storage = await this.storage(b);
|
|
486
|
+
const storage = await this.storage(b, identity);
|
|
420
487
|
const readHead = () => storage.read({ method: "repository.get", repositoryId: b.config.repositoryId, branch: b.config.branch });
|
|
421
488
|
let canonical;
|
|
422
489
|
const account = b.config.repositoryId === `user-workspace-${sha256(b.config.userId).slice(0, 40)}` && b.config.branch === "main";
|
|
@@ -492,8 +559,14 @@ class WorkspaceAuthority {
|
|
|
492
559
|
if (expectedBase === void 0) {
|
|
493
560
|
if ((await fs.readdir(b.config.cwd)).length)
|
|
494
561
|
throw new WorkspaceError("nonempty_workbench", "Workbench changed during hydration; no overwrite");
|
|
495
|
-
|
|
562
|
+
if (this.linkedWorkbench(b)) await fs.rm(staged, { recursive: true });
|
|
563
|
+
else for (const name of await fs.readdir(staged)) await fs.rename(path.join(staged, name), path.join(b.config.cwd, name));
|
|
496
564
|
await this.installGitPolicy(b, head);
|
|
565
|
+
} else if (this.linkedWorkbench(b)) {
|
|
566
|
+
await this.cleanRefreshBase(b, expectedBase);
|
|
567
|
+
await git(b.repo, ["update-ref", `refs/heads/${b.config.branch}`, head, expectedBase]);
|
|
568
|
+
await git(b.config.cwd, ["reset", "--hard", head]);
|
|
569
|
+
await fs.rm(staged, { recursive: true });
|
|
497
570
|
} else {
|
|
498
571
|
await this.cleanRefreshBase(b, expectedBase);
|
|
499
572
|
const original = await selectedTree(b.repo, expectedBase);
|
|
@@ -664,7 +737,7 @@ class WorkspaceAuthority {
|
|
|
664
737
|
sourceBytes(Buffer.from(readme));
|
|
665
738
|
if (b.state.initialized || (await fs.readdir(b.config.cwd)).length)
|
|
666
739
|
throw new WorkspaceError("nonempty_workbench", "Seed only an initially empty approved workbench");
|
|
667
|
-
const storage = await this.storage(b);
|
|
740
|
+
const storage = await this.storage(b, identity);
|
|
668
741
|
const operationId = `ws-${randomUUID()}`;
|
|
669
742
|
b.state.blocked = {
|
|
670
743
|
code: "seed_unknown",
|
|
@@ -684,13 +757,34 @@ class WorkspaceAuthority {
|
|
|
684
757
|
b.state.initialized = true;
|
|
685
758
|
b.state.blocked = null;
|
|
686
759
|
await this.save(b);
|
|
687
|
-
const result = await this.publishIdle(b);
|
|
760
|
+
const result = await this.publishIdle(b, identity);
|
|
761
|
+
if (this.linkedWorkbench(b)) {
|
|
762
|
+
await fs.unlink(path.join(b.config.cwd, ".gitignore"));
|
|
763
|
+
await fs.unlink(path.join(b.config.cwd, "README.md"));
|
|
764
|
+
}
|
|
688
765
|
await this.installGitPolicy(b, result.head);
|
|
689
766
|
return result;
|
|
690
767
|
});
|
|
691
768
|
}
|
|
692
769
|
async installGitPolicy(b, head) {
|
|
693
770
|
delete b.state.publishedMetadata;
|
|
771
|
+
if (this.linkedWorkbench(b)) {
|
|
772
|
+
const destination2 = path.join(b.config.cwd, ".git");
|
|
773
|
+
const exists = await fs.lstat(destination2).then(() => true, (error) => {
|
|
774
|
+
if (error.code === "ENOENT") return false;
|
|
775
|
+
throw error;
|
|
776
|
+
});
|
|
777
|
+
await git(b.repo, ["update-ref", `refs/heads/${b.config.branch}`, head]);
|
|
778
|
+
if (!exists) {
|
|
779
|
+
if ((await fs.readdir(b.config.cwd)).length) throw new WorkspaceError("nonempty_workbench", "Linked worktree destination must be empty");
|
|
780
|
+
await git(b.repo, ["worktree", "add", "--force", b.config.cwd, b.config.branch]);
|
|
781
|
+
}
|
|
782
|
+
const remote2 = `r5d-canonical://${this.config.installationId}/${b.config.userId}/${b.config.repositoryId}`;
|
|
783
|
+
await git(b.repo, ["config", "remote.canonical.url", remote2]);
|
|
784
|
+
await git(b.repo, ["config", "remote.canonical.pushurl", remote2]);
|
|
785
|
+
await git(b.repo, ["config", "protocol.allow", "never"]);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
694
788
|
const destination = path.join(b.config.cwd, ".git");
|
|
695
789
|
await git(b.repo, ["read-tree", head]);
|
|
696
790
|
await fs.mkdir(destination, { mode: 448 });
|
|
@@ -742,22 +836,23 @@ class WorkspaceAuthority {
|
|
|
742
836
|
const input = WorkspaceFileWrite.parse(raw), bytes = Buffer.from(input.base64, "base64");
|
|
743
837
|
if (bytes.toString("base64") !== input.base64 || bytes.length > 768 * 1024)
|
|
744
838
|
throw new WorkspaceError("invalid_input", "Expected canonical base64 for at most 768 KiB", true);
|
|
745
|
-
const b = await this.bench(identity), target = path.join(b.config.cwd, input.path);
|
|
839
|
+
const b = await this.bench(identity), target = input.hostPath ? this.resolveHostPath(input.path) : path.join(b.config.cwd, input.path);
|
|
746
840
|
const fingerprint = { ...input, userId: identity.userId, sessionId: identity.sessionId };
|
|
747
841
|
let mode = 420;
|
|
748
842
|
const check = async () => {
|
|
749
|
-
|
|
843
|
+
if (b.state.blocked) throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
|
|
750
844
|
if (!b.state.initialized) throw new WorkspaceError("uninitialized", "Hydrate this workspace before writing files");
|
|
751
|
-
let current = b.config.cwd;
|
|
752
|
-
|
|
845
|
+
let current = input.hostPath ? path.parse(target).root : b.config.cwd;
|
|
846
|
+
const parentParts = (input.hostPath ? target.slice(current.length) : input.path).split(path.sep).filter(Boolean).slice(0, -1);
|
|
847
|
+
for (const part of parentParts) {
|
|
753
848
|
current = path.join(current, part);
|
|
754
|
-
const stat = await fs.lstat(current).catch((error) => {
|
|
849
|
+
const stat = await (input.hostPath ? fs.stat(current) : fs.lstat(current)).catch((error) => {
|
|
755
850
|
if (error.code === "ENOENT") return null;
|
|
756
851
|
throw error;
|
|
757
852
|
});
|
|
758
|
-
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) throw new WorkspaceError("unsafe_path", "File parent is not a
|
|
853
|
+
if (stat && (!stat.isDirectory() || !input.hostPath && stat.isSymbolicLink())) throw new WorkspaceError("unsafe_path", "File parent is not a directory");
|
|
759
854
|
}
|
|
760
|
-
const previous = await readRegular(target, 8 * 1024 * 1024).catch((error) => {
|
|
855
|
+
const previous = await (input.hostPath ? readHostRegular : readRegular)(target, 8 * 1024 * 1024).catch((error) => {
|
|
761
856
|
if (error.code === "ENOENT") return null;
|
|
762
857
|
throw error;
|
|
763
858
|
});
|
|
@@ -770,7 +865,7 @@ class WorkspaceAuthority {
|
|
|
770
865
|
b.state.blocked = { code: "file_write_unknown", operationId: input.id, message: "Inspect the original file write receipt before changing this workbench" };
|
|
771
866
|
await this.save(b);
|
|
772
867
|
await fs.mkdir(path.dirname(target), { recursive: true, mode: 493 });
|
|
773
|
-
await noSymlinkAncestors(path.dirname(target));
|
|
868
|
+
if (!input.hostPath) await noSymlinkAncestors(path.dirname(target));
|
|
774
869
|
const temporary = path.join(path.dirname(target), `.r5d-file-${randomUUID()}`);
|
|
775
870
|
const file = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, mode);
|
|
776
871
|
try {
|
|
@@ -779,7 +874,7 @@ class WorkspaceAuthority {
|
|
|
779
874
|
} finally {
|
|
780
875
|
await file.close();
|
|
781
876
|
}
|
|
782
|
-
const current = await readRegular(target, 8 * 1024 * 1024).catch((error) => {
|
|
877
|
+
const current = await (input.hostPath ? readHostRegular : readRegular)(target, 8 * 1024 * 1024).catch((error) => {
|
|
783
878
|
if (error.code === "ENOENT") return null;
|
|
784
879
|
throw error;
|
|
785
880
|
});
|
|
@@ -851,7 +946,7 @@ class WorkspaceAuthority {
|
|
|
851
946
|
return this.serial(b, () => this.productAction(b, input.id, { method: "import", url: input.url, branch: input.defaultBranch }, async () => {
|
|
852
947
|
await git(b.repo, ["-c", "protocol.https.allow=always", "fetch", "--no-tags", "--no-recurse-submodules", "--", input.url, `refs/heads/${input.defaultBranch}`], void 0, void 0, { token: input.token });
|
|
853
948
|
const head = GitOid.parse((await git(b.repo, ["rev-parse", "FETCH_HEAD"])).toString().trim());
|
|
854
|
-
return this.importCommit(b, head, input.id);
|
|
949
|
+
return this.importCommit(b, head, input.id, identity);
|
|
855
950
|
}, async () => {
|
|
856
951
|
await this.idle(b);
|
|
857
952
|
if (b.state.initialized || (await fs.readdir(b.config.cwd)).length) throw new WorkspaceError("nonempty_workbench", "Import requires a new empty workbench");
|
|
@@ -893,13 +988,13 @@ class WorkspaceAuthority {
|
|
|
893
988
|
const file = path.join(target.directory, `clone-${randomUUID()}.bundle`);
|
|
894
989
|
await fs.writeFile(file, prepared.bytes, { mode: 384, flag: "wx" });
|
|
895
990
|
await git(target.repo, ["bundle", "unbundle", file]);
|
|
896
|
-
return this.importCommit(target, prepared.head, input.id);
|
|
991
|
+
return this.importCommit(target, prepared.head, input.id, identity);
|
|
897
992
|
}, async () => {
|
|
898
993
|
await this.idle(target);
|
|
899
994
|
if (target.state.initialized || (await fs.readdir(target.config.cwd)).length) throw new WorkspaceError("nonempty_workbench", "Branch already has a workbench");
|
|
900
995
|
}));
|
|
901
996
|
}
|
|
902
|
-
async importCommit(b, head, id) {
|
|
997
|
+
async importCommit(b, head, id, identity) {
|
|
903
998
|
const entries = await selectedTree(b.repo, head);
|
|
904
999
|
const staged = path.join(b.directory, `import-${randomUUID()}`);
|
|
905
1000
|
await fs.mkdir(staged, { mode: 448 });
|
|
@@ -927,7 +1022,7 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
927
1022
|
}
|
|
928
1023
|
const tree = await snapshotTree(b.repo, staged, head);
|
|
929
1024
|
if (tree !== (await git(b.repo, ["rev-parse", `${head}^{tree}`])).toString().trim()) head = (await git(b.repo, ["commit-tree", tree, "-p", head], "Configure workspace exclusions\n")).toString().trim();
|
|
930
|
-
const storage = await this.storage(b), operationId = `product-${sha256(id).slice(0, 48)}`;
|
|
1025
|
+
const storage = await this.storage(b, identity), operationId = `product-${sha256(id).slice(0, 48)}`;
|
|
931
1026
|
let existing;
|
|
932
1027
|
try {
|
|
933
1028
|
existing = await storage.read({ method: "repository.get", repositoryId: b.config.repositoryId, branch: b.config.branch });
|
|
@@ -949,7 +1044,8 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
949
1044
|
}
|
|
950
1045
|
const result = await storage.mutate({ method: "repository.publish", operationId, repositoryId: b.config.repositoryId, branch: b.config.branch, expectedHead: null, commit: head, bundleId: blobId });
|
|
951
1046
|
if (result.head !== head) throw new WorkspaceError("invalid_receipt", "Import receipt does not match selected commit");
|
|
952
|
-
|
|
1047
|
+
if (this.linkedWorkbench(b)) await fs.rm(staged, { recursive: true });
|
|
1048
|
+
else for (const name of await fs.readdir(staged)) await fs.rename(path.join(staged, name), path.join(b.config.cwd, name));
|
|
953
1049
|
await this.installGitPolicy(b, head);
|
|
954
1050
|
b.state.initialized = true;
|
|
955
1051
|
b.state.head = head;
|
|
@@ -961,7 +1057,11 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
961
1057
|
const b = await this.bench(identity);
|
|
962
1058
|
if (!input.message.trim() || input.message.length > 1e4) throw new WorkspaceError("invalid_input", "Commit message is required");
|
|
963
1059
|
return this.serial(b, () => this.productAction(b, input.id, { method: "commit", expectedHead: input.expectedHead, message: input.message }, async () => {
|
|
964
|
-
const result = await this.publishIdle(b, input.message);
|
|
1060
|
+
const result = await this.publishIdle(b, identity, input.message);
|
|
1061
|
+
if (this.linkedWorkbench(b)) {
|
|
1062
|
+
await git(b.config.cwd, ["reset", "--mixed", result.head]);
|
|
1063
|
+
return { ...result, treeHash: (await git(b.repo, ["rev-parse", `${result.head}^{tree}`])).toString().trim() };
|
|
1064
|
+
}
|
|
965
1065
|
const metadata = path.join(b.config.cwd, ".git");
|
|
966
1066
|
await noSymlinkAncestors(metadata);
|
|
967
1067
|
const retained = path.join(b.directory, `commit-retained-${randomUUID()}`);
|
|
@@ -992,9 +1092,16 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
992
1092
|
async archiveBranch(identity, input) {
|
|
993
1093
|
const b = await this.bench(identity);
|
|
994
1094
|
return this.serial(b, () => this.productAction(b, input.id, { method: "archive" }, async () => {
|
|
995
|
-
const storage = await this.storage(b);
|
|
1095
|
+
const storage = await this.storage(b, identity);
|
|
996
1096
|
const current = await storage.read({ method: "repository.get", repositoryId: b.config.repositoryId, branch: b.config.branch });
|
|
997
1097
|
const result = await storage.mutate({ method: "repository.archive", operationId: `archive-${sha256(input.id).slice(0, 48)}`, repositoryId: b.config.repositoryId, branch: b.config.branch, expectedHead: current.head });
|
|
1098
|
+
if (this.linkedWorkbench(b)) {
|
|
1099
|
+
const retained = path.join(b.directory, `archive-retained-${randomUUID()}`, "source");
|
|
1100
|
+
await fs.mkdir(path.dirname(retained), { recursive: true, mode: 448 });
|
|
1101
|
+
await fs.cp(b.config.cwd, retained, { recursive: true, filter: (source) => path.basename(source) !== ".git" });
|
|
1102
|
+
await git(b.repo, ["worktree", "remove", "--force", b.config.cwd]);
|
|
1103
|
+
await git(b.repo, ["branch", "-D", b.config.branch]);
|
|
1104
|
+
}
|
|
998
1105
|
b.state.blocked = { code: "archived", message: "This branch incarnation is archived; its workbench and source are retained" };
|
|
999
1106
|
await this.save(b);
|
|
1000
1107
|
return { removed: true, receipt: result };
|
|
@@ -1007,7 +1114,7 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1007
1114
|
await this.idle(b, true);
|
|
1008
1115
|
if (input.expectedHead !== void 0 && input.expectedHead !== b.state.head) throw new WorkspaceError("conflict", "Workbench base changed before reset");
|
|
1009
1116
|
if (b.state.blocked?.code === "archived") throw new WorkspaceError("archived", "A deleted branch cannot be reset");
|
|
1010
|
-
const storage = await this.storage(b);
|
|
1117
|
+
const storage = await this.storage(b, identity);
|
|
1011
1118
|
if (b.state.blocked?.operationId) {
|
|
1012
1119
|
const receipt = await storage.read({ method: "operation.get", lookupId: b.state.blocked.operationId });
|
|
1013
1120
|
if (receipt.state !== "completed" || b.state.blocked.commit && receipt.result?.head !== b.state.blocked.commit) throw new WorkspaceError("publication_unknown", "Reconcile the original publication before resetting");
|
|
@@ -1020,18 +1127,23 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1020
1127
|
b.state.blocked = { code: "reset_incomplete", message: "Reset source retained; complete hydration after inspection" };
|
|
1021
1128
|
await this.save(b);
|
|
1022
1129
|
await noSymlinkAncestors(b.config.cwd);
|
|
1023
|
-
|
|
1130
|
+
if (this.linkedWorkbench(b)) {
|
|
1131
|
+
await fs.cp(b.config.cwd, path.join(retained, "source"), { recursive: true, filter: (source) => path.basename(source) !== ".git" });
|
|
1132
|
+
await git(b.repo, ["worktree", "remove", "--force", b.config.cwd]);
|
|
1133
|
+
await git(b.repo, ["branch", "-D", b.config.branch]);
|
|
1134
|
+
} else await fs.rename(b.config.cwd, path.join(retained, "source"));
|
|
1024
1135
|
await fs.mkdir(b.config.cwd, { mode: 448 });
|
|
1025
1136
|
b.state.initialized = false;
|
|
1026
1137
|
b.state.head = null;
|
|
1027
1138
|
b.state.blocked = null;
|
|
1028
1139
|
await this.save(b);
|
|
1029
|
-
const result = await this.hydrateIdle(b);
|
|
1140
|
+
const result = await this.hydrateIdle(b, identity);
|
|
1030
1141
|
return { ...result, reset: true, retained: true };
|
|
1031
1142
|
}));
|
|
1032
1143
|
}
|
|
1033
1144
|
async publish(identity) {
|
|
1034
1145
|
const b = await this.bench(identity);
|
|
1146
|
+
if (b.config.rootProfile === "account") return { head: "", unchanged: true };
|
|
1035
1147
|
return this.serial(b, async () => {
|
|
1036
1148
|
try {
|
|
1037
1149
|
await this.idle(b);
|
|
@@ -1039,12 +1151,12 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1039
1151
|
if (!(error instanceof WorkspaceError) || error.code !== "workbench_busy") throw error;
|
|
1040
1152
|
throw new WorkspacePublicationNotAdmitted("workbench_busy", error.message);
|
|
1041
1153
|
}
|
|
1042
|
-
return this.publishIdle(b);
|
|
1154
|
+
return this.publishIdle(b, identity);
|
|
1043
1155
|
});
|
|
1044
1156
|
}
|
|
1045
|
-
async publishIdle(b, message = "Destination workspace snapshot") {
|
|
1157
|
+
async publishIdle(b, identity, message = "Destination workspace snapshot") {
|
|
1046
1158
|
if (!b.state.initialized) throw new WorkspaceError("not_initialized", "Hydrate or explicitly seed first");
|
|
1047
|
-
const storage = await this.storage(b);
|
|
1159
|
+
const storage = await this.storage(b, identity);
|
|
1048
1160
|
const latest = await storage.read({
|
|
1049
1161
|
method: "repository.get",
|
|
1050
1162
|
repositoryId: b.config.repositoryId,
|
|
@@ -1069,6 +1181,10 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1069
1181
|
const bytes = await readRegular(bundleFile, STORAGE_LIMITS.blobBytes);
|
|
1070
1182
|
const operationId = `ws-${randomUUID()}`, blobId = `ws-${randomUUID()}`;
|
|
1071
1183
|
try {
|
|
1184
|
+
if (this.linkedWorkbench(b)) {
|
|
1185
|
+
delete b.state.publishedMetadata;
|
|
1186
|
+
throw new Error("linked_worktree_metadata_is_owned_by_git");
|
|
1187
|
+
}
|
|
1072
1188
|
const metadata = path.join(b.config.cwd, ".git"), ref = `refs/heads/${b.config.branch}`;
|
|
1073
1189
|
const localHead = (await readRegular(path.join(metadata, ref), 1024)).toString().trim();
|
|
1074
1190
|
const index = await readRegular(path.join(metadata, "index"), 8 * 1024 * 1024);
|
|
@@ -1115,6 +1231,10 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1115
1231
|
});
|
|
1116
1232
|
if (result.head !== commit)
|
|
1117
1233
|
throw new WorkspaceError("invalid_response", "Publication receipt head mismatch; inspect original operation");
|
|
1234
|
+
if (this.linkedWorkbench(b) && await fs.lstat(path.join(b.config.cwd, ".git")).then(() => true, (error) => {
|
|
1235
|
+
if (error.code === "ENOENT") return false;
|
|
1236
|
+
throw error;
|
|
1237
|
+
})) await git(b.config.cwd, ["reset", "--mixed", commit]);
|
|
1118
1238
|
b.state.head = commit;
|
|
1119
1239
|
b.state.blocked = null;
|
|
1120
1240
|
await this.save(b);
|
|
@@ -1127,7 +1247,7 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1127
1247
|
const blocked = b.state.blocked;
|
|
1128
1248
|
if (blocked?.code !== "publication_unknown" || !blocked.operationId || !blocked.commit)
|
|
1129
1249
|
throw new WorkspaceError("not_reconcilable", "Only a verified completed publication receipt can resolve automatically");
|
|
1130
|
-
const storage = await this.storage(b);
|
|
1250
|
+
const storage = await this.storage(b, identity);
|
|
1131
1251
|
const receipt = await storage.read({
|
|
1132
1252
|
method: "operation.get",
|
|
1133
1253
|
lookupId: blocked.operationId
|
|
@@ -1172,7 +1292,7 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1172
1292
|
}
|
|
1173
1293
|
async materializeArtifact(identity, input) {
|
|
1174
1294
|
const b = await this.bench(identity);
|
|
1175
|
-
return this.serial(b, () => new SessionArtifactStore(this.config.root).materialize(identity, input));
|
|
1295
|
+
return this.serial(b, () => new SessionArtifactStore(this.config.workspaceRoot ?? this.config.root).materialize(identity, input));
|
|
1176
1296
|
}
|
|
1177
1297
|
/** Default arbitrary shell is mutating. Exact trusted config approval is the sole exemption. */
|
|
1178
1298
|
async start(identity, input) {
|
|
@@ -1187,7 +1307,7 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1187
1307
|
if (RESERVED_ARTIFACT_ENV.some((name) => name in payload.env)) throw new WorkspaceError("reserved_environment", "R5D_ROOT and R5D_SESSION_ID are set by the workspace authority");
|
|
1188
1308
|
const requestedEnv = payload.env, prior = b.state.runs[operation.operationId];
|
|
1189
1309
|
if (!prior || prior.artifactEnvironment) {
|
|
1190
|
-
payload.env = { ...payload.env, ...await new SessionArtifactStore(this.config.root).environment(identity) };
|
|
1310
|
+
payload.env = { ...payload.env, ...await new SessionArtifactStore(this.config.workspaceRoot ?? this.config.root).environment(identity) };
|
|
1191
1311
|
operation.payload = payload;
|
|
1192
1312
|
}
|
|
1193
1313
|
if (payload.cwd !== b.config.cwd) throw new WorkspaceError("forbidden_cwd", "Shell cwd must be exact approved workbench");
|
|
@@ -1211,18 +1331,6 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1211
1331
|
await this.save(b);
|
|
1212
1332
|
return receipt;
|
|
1213
1333
|
}
|
|
1214
|
-
try {
|
|
1215
|
-
await this.idle(b);
|
|
1216
|
-
} catch (error) {
|
|
1217
|
-
if (!(error instanceof WorkspaceError) || error.code !== "workbench_busy") throw error;
|
|
1218
|
-
b.state.runs[operation.operationId] = { artifactEnvironment: true, sessionId: identity.sessionId, payloadHash: hash, mutating: false, state: "rejected_capacity" };
|
|
1219
|
-
await this.save(b);
|
|
1220
|
-
throw new ExecutorError(
|
|
1221
|
-
"capacity_busy",
|
|
1222
|
-
"Workspace is busy; poll the original command until terminal before choosing new work",
|
|
1223
|
-
true
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
1334
|
const nonmutating = Object.keys(requestedEnv).length === 0 && payload.credentials.length === 0 && b.config.nonmutatingArgv.some((argv) => canonicalJson(argv) === canonicalJson(payload.argv));
|
|
1227
1335
|
b.state.runs[operation.operationId] = { artifactEnvironment: true, sessionId: identity.sessionId, payloadHash: hash, mutating: !nonmutating, state: "unknown", argv: payload.argv, cwd: payload.cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1228
1336
|
await this.save(b);
|
|
@@ -10,6 +10,9 @@ const ApprovedWorkbench = z.object({
|
|
|
10
10
|
repositoryId: StorageId,
|
|
11
11
|
branch: BranchName,
|
|
12
12
|
cwd: z.string().refine(path.isAbsolute),
|
|
13
|
+
rootProfile: z.enum(["account", "project"]).default("project"),
|
|
14
|
+
namespace: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/).optional(),
|
|
15
|
+
projectName: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/).optional(),
|
|
13
16
|
// Supervisor-owned exact argv approvals, NOT an agent-supplied read-only flag.
|
|
14
17
|
// Approved servers must keep generated/cache files ignored and never write source.
|
|
15
18
|
nonmutatingArgv: z.array(z.array(z.string()).min(1)).max(20).default([])
|
|
@@ -17,6 +20,7 @@ const ApprovedWorkbench = z.object({
|
|
|
17
20
|
const WorkspaceConfig = z.object({
|
|
18
21
|
installationId: RuntimeId,
|
|
19
22
|
root: z.string().refine(path.isAbsolute),
|
|
23
|
+
workspaceRoot: z.string().refine(path.isAbsolute).optional(),
|
|
20
24
|
workbenches: z.array(ApprovedWorkbench).max(100),
|
|
21
25
|
dynamicWorkbenches: z.boolean().optional()
|
|
22
26
|
}).strict();
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
3
|
import { safeTreePath } from "./storage-wire.mjs";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
const WorkspaceFileWrite = z.object({
|
|
5
6
|
method: z.literal("fileWrite"),
|
|
6
7
|
id: RuntimeId,
|
|
7
|
-
path: z.string().min(1).max(4096)
|
|
8
|
-
|
|
9
|
-
safeTreePath(value);
|
|
10
|
-
return true;
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
}),
|
|
8
|
+
path: z.string().min(1).max(4096),
|
|
9
|
+
hostPath: z.boolean().optional(),
|
|
15
10
|
expectedSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
16
11
|
intentHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
17
12
|
base64: z.string().max(1048576)
|
|
18
|
-
}).strict()
|
|
13
|
+
}).strict().superRefine((value, ctx) => {
|
|
14
|
+
if (value.hostPath) {
|
|
15
|
+
if (!path.isAbsolute(value.path) && !value.path.startsWith("~/") || value.path.includes("\0"))
|
|
16
|
+
ctx.addIssue({ code: "custom", path: ["path"], message: "Host paths must be absolute or home-relative" });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
safeTreePath(value.path);
|
|
21
|
+
} catch {
|
|
22
|
+
ctx.addIssue({ code: "custom", path: ["path"], message: "Workspace paths must be safe and relative" });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
19
25
|
const WorkspaceFileWriteLookup = z.object({ method: z.literal("fileWriteResult"), id: RuntimeId }).strict();
|
|
20
26
|
export {
|
|
21
27
|
WorkspaceFileWrite,
|
|
@@ -23,6 +23,12 @@ async function noSymlinkAncestors(file) {
|
|
|
23
23
|
}
|
|
24
24
|
async function readRegular(file, limit = 32 * 1024 * 1024) {
|
|
25
25
|
await noSymlinkAncestors(file);
|
|
26
|
+
return readRegularFile(file, limit);
|
|
27
|
+
}
|
|
28
|
+
async function readHostRegular(file, limit = 32 * 1024 * 1024) {
|
|
29
|
+
return readRegularFile(file, limit);
|
|
30
|
+
}
|
|
31
|
+
async function readRegularFile(file, limit) {
|
|
26
32
|
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
27
33
|
try {
|
|
28
34
|
const st = await handle.stat();
|
|
@@ -263,9 +269,19 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
263
269
|
}
|
|
264
270
|
)) {
|
|
265
271
|
await noSymlinkAncestors(metadata);
|
|
266
|
-
|
|
272
|
+
const metadataStat = await fs.lstat(metadata);
|
|
273
|
+
let gitDirectory = metadata;
|
|
274
|
+
if (metadataStat.isFile()) {
|
|
275
|
+
const marker = (await readRegular(metadata, 4096)).toString("utf8").trim();
|
|
276
|
+
const match = /^gitdir: (.+)$/.exec(marker);
|
|
277
|
+
if (!match || !path.isAbsolute(match[1])) throw new WorkspaceError("unsafe_git", "Invalid linked worktree metadata");
|
|
278
|
+
gitDirectory = path.normalize(match[1]);
|
|
279
|
+
const worktrees = path.join(repo, "worktrees") + path.sep;
|
|
280
|
+
if (!gitDirectory.startsWith(worktrees)) throw new WorkspaceError("unsafe_git", "Linked worktree belongs to another repository");
|
|
281
|
+
await noSymlinkAncestors(gitDirectory);
|
|
282
|
+
} else if (!metadataStat.isDirectory()) throw new WorkspaceError("unsafe_git", "Invalid worktree metadata");
|
|
267
283
|
for (const name of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "rebase-merge", "rebase-apply", "index.lock"]) {
|
|
268
|
-
if (await fs.lstat(path.join(
|
|
284
|
+
if (await fs.lstat(path.join(gitDirectory, name)).then(
|
|
269
285
|
() => true,
|
|
270
286
|
(e) => {
|
|
271
287
|
if (e.code === "ENOENT") return false;
|
|
@@ -277,7 +293,7 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
277
293
|
"Git merge/rebase/index operation remains in progress; preserve and resolve it before publication"
|
|
278
294
|
);
|
|
279
295
|
}
|
|
280
|
-
const index = path.join(
|
|
296
|
+
const index = path.join(gitDirectory, "index");
|
|
281
297
|
if (await fs.lstat(index).then(
|
|
282
298
|
() => true,
|
|
283
299
|
(e) => {
|
|
@@ -326,6 +342,7 @@ export {
|
|
|
326
342
|
git,
|
|
327
343
|
noSymlinkAncestors,
|
|
328
344
|
privateRoot,
|
|
345
|
+
readHostRegular,
|
|
329
346
|
readRegular,
|
|
330
347
|
selectedTree,
|
|
331
348
|
sha256,
|
|
@@ -46,21 +46,36 @@ export declare const PersonalWorkspaceRequest: z.ZodObject<{
|
|
|
46
46
|
repositoryId: z.ZodString;
|
|
47
47
|
branch: z.ZodString;
|
|
48
48
|
sessionId: z.ZodString;
|
|
49
|
-
|
|
49
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
50
|
+
account: "account";
|
|
51
|
+
project: "project";
|
|
52
|
+
}>>;
|
|
53
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
54
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
50
55
|
}, z.core.$strict>;
|
|
51
56
|
sourceWorkbench: z.ZodOptional<z.ZodObject<{
|
|
52
57
|
id: z.ZodString;
|
|
53
58
|
repositoryId: z.ZodString;
|
|
54
59
|
branch: z.ZodString;
|
|
55
60
|
sessionId: z.ZodString;
|
|
56
|
-
|
|
61
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
62
|
+
account: "account";
|
|
63
|
+
project: "project";
|
|
64
|
+
}>>;
|
|
65
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
66
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
57
67
|
}, z.core.$strict>>;
|
|
58
68
|
sharedWorkbenches: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
59
69
|
id: z.ZodString;
|
|
60
70
|
repositoryId: z.ZodString;
|
|
61
71
|
branch: z.ZodString;
|
|
62
72
|
sessionId: z.ZodString;
|
|
63
|
-
|
|
73
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
74
|
+
account: "account";
|
|
75
|
+
project: "project";
|
|
76
|
+
}>>;
|
|
77
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
78
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
64
79
|
}, z.core.$strict>>>;
|
|
65
80
|
kind: z.ZodEnum<{
|
|
66
81
|
inspect: "inspect";
|
|
@@ -14,8 +14,8 @@ export type SessionArtifactChunk = z.infer<typeof SessionArtifactChunk>;
|
|
|
14
14
|
export declare const RESERVED_ARTIFACT_ENV: readonly ["R5D_ROOT", "R5D_SESSION_ID"];
|
|
15
15
|
/** Session artifacts never join repository snapshots or another session's env. */
|
|
16
16
|
export declare class SessionArtifactStore {
|
|
17
|
-
readonly
|
|
18
|
-
constructor(
|
|
17
|
+
readonly workspaceRoot: string;
|
|
18
|
+
constructor(workspaceRoot: string);
|
|
19
19
|
environment(identity: WorkspaceIdentity): Promise<{
|
|
20
20
|
R5D_ROOT: string;
|
|
21
21
|
R5D_SESSION_ID: string;
|
|
@@ -24,8 +24,16 @@ export declare class WorkspaceAuthority {
|
|
|
24
24
|
private closed;
|
|
25
25
|
private closeTask?;
|
|
26
26
|
private readonly actions;
|
|
27
|
+
private readonly repositoryInitializations;
|
|
27
28
|
private pending;
|
|
28
29
|
private constructor();
|
|
30
|
+
private expectedCwd;
|
|
31
|
+
private stableBinding;
|
|
32
|
+
private repositoryPath;
|
|
33
|
+
private ensureRepository;
|
|
34
|
+
private assertNonOverlappingCheckout;
|
|
35
|
+
private linkedWorkbench;
|
|
36
|
+
private resolveHostPath;
|
|
29
37
|
static open(options: WorkspaceAuthorityOptions): Promise<WorkspaceAuthority>;
|
|
30
38
|
private readonly registrations;
|
|
31
39
|
private bench;
|
|
@@ -68,6 +76,7 @@ export declare class WorkspaceAuthority {
|
|
|
68
76
|
inspectFiles(identity: WorkspaceIdentity, command: {
|
|
69
77
|
method: "directory" | "raw";
|
|
70
78
|
path: string;
|
|
79
|
+
hostPath?: boolean;
|
|
71
80
|
}): Promise<{
|
|
72
81
|
path: string;
|
|
73
82
|
base64: string;
|
|
@@ -198,14 +207,17 @@ export declare class WorkspaceAuthority {
|
|
|
198
207
|
completedAt?: string;
|
|
199
208
|
}>;
|
|
200
209
|
workbench: {
|
|
210
|
+
sessionId: string;
|
|
201
211
|
id: string;
|
|
202
212
|
userId: string;
|
|
203
|
-
sessionId: string;
|
|
204
213
|
repositoryId: string;
|
|
205
214
|
branch: string;
|
|
206
215
|
cwd: string;
|
|
216
|
+
rootProfile: "account" | "project";
|
|
207
217
|
nonmutatingArgv: string[][];
|
|
208
218
|
sharedSessionId?: string | undefined;
|
|
219
|
+
namespace?: string | undefined;
|
|
220
|
+
projectName?: string | undefined;
|
|
209
221
|
};
|
|
210
222
|
}>;
|
|
211
223
|
private cleanRefreshBase;
|
|
@@ -213,6 +225,9 @@ export declare class WorkspaceAuthority {
|
|
|
213
225
|
* idle workbench refresh; all replaced source/index/ref bytes are retained. */
|
|
214
226
|
hydrate(identity: WorkspaceIdentity, expectedBase?: string): Promise<{
|
|
215
227
|
head: string;
|
|
228
|
+
} | {
|
|
229
|
+
head: string;
|
|
230
|
+
unchanged: boolean;
|
|
216
231
|
}>;
|
|
217
232
|
private hydrateIdle;
|
|
218
233
|
private assertFreshAccountWorkbench;
|
|
@@ -9,12 +9,19 @@ export declare const ApprovedWorkbench: z.ZodObject<{
|
|
|
9
9
|
repositoryId: z.ZodString;
|
|
10
10
|
branch: z.ZodString;
|
|
11
11
|
cwd: z.ZodString;
|
|
12
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
13
|
+
account: "account";
|
|
14
|
+
project: "project";
|
|
15
|
+
}>>;
|
|
16
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
17
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
12
18
|
nonmutatingArgv: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodString>>>;
|
|
13
19
|
}, z.core.$strict>;
|
|
14
20
|
export type ApprovedWorkbench = z.input<typeof ApprovedWorkbench>;
|
|
15
21
|
export declare const WorkspaceConfig: z.ZodObject<{
|
|
16
22
|
installationId: z.ZodString;
|
|
17
23
|
root: z.ZodString;
|
|
24
|
+
workspaceRoot: z.ZodOptional<z.ZodString>;
|
|
18
25
|
workbenches: z.ZodArray<z.ZodObject<{
|
|
19
26
|
id: z.ZodString;
|
|
20
27
|
userId: z.ZodString;
|
|
@@ -23,6 +30,12 @@ export declare const WorkspaceConfig: z.ZodObject<{
|
|
|
23
30
|
repositoryId: z.ZodString;
|
|
24
31
|
branch: z.ZodString;
|
|
25
32
|
cwd: z.ZodString;
|
|
33
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
34
|
+
account: "account";
|
|
35
|
+
project: "project";
|
|
36
|
+
}>>;
|
|
37
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
38
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
26
39
|
nonmutatingArgv: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodString>>>;
|
|
27
40
|
}, z.core.$strict>>;
|
|
28
41
|
dynamicWorkbenches: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -3,6 +3,7 @@ export declare const WorkspaceFileWrite: z.ZodObject<{
|
|
|
3
3
|
method: z.ZodLiteral<"fileWrite">;
|
|
4
4
|
id: z.ZodString;
|
|
5
5
|
path: z.ZodString;
|
|
6
|
+
hostPath: z.ZodOptional<z.ZodBoolean>;
|
|
6
7
|
expectedSha256: z.ZodNullable<z.ZodString>;
|
|
7
8
|
intentHash: z.ZodOptional<z.ZodString>;
|
|
8
9
|
base64: z.ZodString;
|
|
@@ -2,6 +2,9 @@ export declare const sha256: (bytes: Buffer | string) => string;
|
|
|
2
2
|
export declare function privateRoot(root: string, installationId: string): Promise<void>;
|
|
3
3
|
export declare function noSymlinkAncestors(file: string): Promise<void>;
|
|
4
4
|
export declare function readRegular(file: string, limit?: number): Promise<Buffer>;
|
|
5
|
+
/** Host-wide tools follow ordinary directory symlinks while refusing a final
|
|
6
|
+
* symlink and retaining the same bounded, single-link regular-file contract. */
|
|
7
|
+
export declare function readHostRegular(file: string, limit?: number): Promise<Buffer>;
|
|
5
8
|
/** File-only migration capsules can omit Git's empty structural directories.
|
|
6
9
|
* Restore only that directory skeleton after confirming the authority-owned
|
|
7
10
|
* repository markers; refs, objects, indexes and configuration are untouched. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.142",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/mjs/main.mjs",
|
|
6
6
|
"module": "./dist/mjs/main.mjs",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"r5d-worker": "dist/mjs/main.mjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@ricsam/r5d-api": "^0.0.
|
|
25
|
-
"@ricsam/r5dctl": "0.0.
|
|
24
|
+
"@ricsam/r5d-api": "^0.0.142",
|
|
25
|
+
"@ricsam/r5dctl": "0.0.142",
|
|
26
26
|
"node-pty": "1.1.0",
|
|
27
27
|
"zod": "^4.1.13",
|
|
28
28
|
"picomatch": "^4.0.3"
|