@skrr-ai/cli 0.1.87 → 0.1.89
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/agents/chat.js +27 -1
- package/dist/commands/computer/index.d.ts +19 -0
- package/dist/commands/computer/index.js +37 -0
- package/dist/commands/labels/create.d.ts +1 -0
- package/dist/commands/labels/create.js +2 -0
- package/dist/commands/labels/restore.d.ts +1 -0
- package/dist/commands/labels/restore.js +6 -3
- package/dist/commands/labels/update.d.ts +1 -0
- package/dist/commands/labels/update.js +2 -0
- package/dist/commands/machines/computer-adoption.d.ts +16 -0
- package/dist/commands/machines/computer-adoption.js +34 -0
- package/dist/commands/machines/dedicated/agent-browser.d.ts +25 -0
- package/dist/commands/machines/dedicated/agent-browser.js +65 -0
- package/dist/commands/machines/dedicated/archive.d.ts +26 -0
- package/dist/commands/machines/dedicated/archive.js +113 -0
- package/dist/commands/machines/dedicated/audit.d.ts +25 -0
- package/dist/commands/machines/dedicated/audit.js +113 -0
- package/dist/commands/machines/dedicated/computer.d.ts +23 -0
- package/dist/commands/machines/dedicated/computer.js +64 -0
- package/dist/commands/machines/dedicated/cp.d.ts +2 -0
- package/dist/commands/machines/dedicated/cp.js +74 -2
- package/dist/commands/machines/dedicated/git-status.d.ts +15 -0
- package/dist/commands/machines/dedicated/git-status.js +56 -0
- package/dist/commands/machines/dedicated/index.js +15 -1
- package/dist/commands/machines/dedicated/ls.d.ts +27 -0
- package/dist/commands/machines/dedicated/ls.js +100 -0
- package/dist/commands/machines/dedicated/mkdir.d.ts +18 -0
- package/dist/commands/machines/dedicated/mkdir.js +57 -0
- package/dist/commands/machines/dedicated/mv.d.ts +24 -0
- package/dist/commands/machines/dedicated/mv.js +83 -0
- package/dist/commands/machines/dedicated/rm.d.ts +26 -0
- package/dist/commands/machines/dedicated/rm.js +100 -0
- package/dist/commands/machines/dedicated/search.d.ts +24 -0
- package/dist/commands/machines/dedicated/search.js +78 -0
- package/dist/commands/machines/dedicated/show.js +17 -2
- package/dist/commands/machines/dedicated/stat.d.ts +22 -0
- package/dist/commands/machines/dedicated/stat.js +92 -0
- package/dist/commands/machines/hosted/index.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/start.js +1 -1
- package/dist/commands/machines/services/declare.d.ts +24 -0
- package/dist/commands/machines/services/declare.js +73 -0
- package/dist/commands/machines/services/index.d.ts +15 -0
- package/dist/commands/machines/services/index.js +31 -0
- package/dist/commands/machines/services/ls.d.ts +17 -0
- package/dist/commands/machines/services/ls.js +60 -0
- package/dist/commands/machines/services/withdraw.d.ts +19 -0
- package/dist/commands/machines/services/withdraw.js +47 -0
- package/dist/commands/machines/share.d.ts +29 -0
- package/dist/commands/machines/share.js +100 -0
- package/dist/commands/machines/shared.d.ts +19 -0
- package/dist/commands/machines/shared.js +62 -0
- package/dist/commands/machines/shares.d.ts +16 -0
- package/dist/commands/machines/shares.js +69 -0
- package/dist/commands/machines/unshare.d.ts +19 -0
- package/dist/commands/machines/unshare.js +61 -0
- package/dist/commands/tasks/labels/create.d.ts +1 -0
- package/dist/commands/tasks/labels/create.js +3 -0
- package/dist/commands/views/create.d.ts +1 -0
- package/dist/commands/views/create.js +5 -0
- package/dist/lib/agent-home-workspace.d.ts +7 -0
- package/dist/lib/agent-home-workspace.js +147 -0
- package/dist/lib/computer-adoption.d.ts +60 -0
- package/dist/lib/computer-adoption.js +72 -0
- package/dist/lib/computer-consent.d.ts +55 -0
- package/dist/lib/computer-consent.js +154 -0
- package/dist/lib/computer-files.d.ts +239 -0
- package/dist/lib/computer-files.js +707 -0
- package/dist/lib/dedicated-copy.d.ts +45 -9
- package/dist/lib/dedicated-copy.js +141 -44
- package/dist/lib/dedicated-machines.d.ts +80 -0
- package/dist/lib/dedicated-machines.js +137 -6
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +16 -2
- package/dist/lib/label-scope.d.ts +12 -0
- package/dist/lib/label-scope.js +15 -1
- package/dist/lib/machine-audit.d.ts +34 -0
- package/dist/lib/machine-audit.js +50 -0
- package/dist/lib/machine-grants.d.ts +70 -0
- package/dist/lib/machine-grants.js +47 -0
- package/dist/lib/machine-services.d.ts +64 -0
- package/dist/lib/machine-services.js +60 -0
- package/dist/lib/task-view-render.d.ts +2 -0
- package/dist/lib/task-view-render.js +1 -1
- package/dist/lib/views/vocabulary.d.ts +1 -1
- package/dist/lib/views/vocabulary.js +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +1050 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +1196 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +1050 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +1175 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +22391 -20727
- package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
- package/oclif.manifest.json +14767 -13095
- package/package.json +2 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ComputerFileEntityTag } from '@skrr-ai/data-provider';
|
|
2
|
+
import { statComputerFile } from './computer-files';
|
|
1
3
|
import { readDedicatedRuntimeFileChunk, writeDedicatedRuntimeFileChunk, type DedicatedRuntimeScope } from './dedicated-machines';
|
|
2
4
|
/**
|
|
3
5
|
* The two copy loops behind `skrr machines dedicated cp`.
|
|
@@ -8,6 +10,14 @@ import { readDedicatedRuntimeFileChunk, writeDedicatedRuntimeFileChunk, type Ded
|
|
|
8
10
|
* stages beside its destination on the machine and the final chunk renames it;
|
|
9
11
|
* a download writes a temporary file beside its local destination and renames.
|
|
10
12
|
*
|
|
13
|
+
* Every chunk presents the ONE lease generation the command resolved before it
|
|
14
|
+
* started (review C-01), so a machine replaced mid-copy refuses the remaining
|
|
15
|
+
* chunks instead of receiving them. And an upload never replaces a destination
|
|
16
|
+
* it was not told to (review CW-01): it stats the destination first, creates a
|
|
17
|
+
* new file only if the name is still free at commit (`createOnly`), and
|
|
18
|
+
* replaces an existing one only when asked to — and then only the exact version
|
|
19
|
+
* it saw (`replaceTag`).
|
|
20
|
+
*
|
|
11
21
|
* And neither leaves its partial behind when it is cancelled (OSK-8708).
|
|
12
22
|
* `listenForCopyInterrupt` turns SIGINT, SIGTERM and SIGHUP into an
|
|
13
23
|
* AbortSignal the loops honour; a download also removes its temporary file from
|
|
@@ -16,12 +26,25 @@ import { readDedicatedRuntimeFileChunk, writeDedicatedRuntimeFileChunk, type Ded
|
|
|
16
26
|
export interface DedicatedCopyIo {
|
|
17
27
|
readChunk: typeof readDedicatedRuntimeFileChunk;
|
|
18
28
|
writeChunk: typeof writeDedicatedRuntimeFileChunk;
|
|
29
|
+
/** How an upload learns what its destination is before it sends a byte. */
|
|
30
|
+
statPath: typeof statComputerFile;
|
|
19
31
|
chunkBytes: number;
|
|
20
32
|
maxBytes: number;
|
|
21
33
|
/** Run `fn` if the process exits; returns the unregister. Injectable for tests. */
|
|
22
34
|
onProcessExit: (fn: () => void) => () => void;
|
|
23
35
|
}
|
|
24
36
|
export interface DedicatedCopyOptions extends Partial<DedicatedCopyIo> {
|
|
37
|
+
/**
|
|
38
|
+
* The lease generation the command resolved at its start
|
|
39
|
+
* (`resolveDedicatedFileGeneration`), presented on every call of the copy.
|
|
40
|
+
*/
|
|
41
|
+
generation: number;
|
|
42
|
+
/** Upload: replace an existing destination (the version the stat found). */
|
|
43
|
+
overwrite?: boolean;
|
|
44
|
+
/** Upload: replace exactly this version of the destination, and no other. */
|
|
45
|
+
ifTag?: ComputerFileEntityTag;
|
|
46
|
+
/** Upload: told which version is about to be replaced, before any byte is sent. */
|
|
47
|
+
onReplace?: (destination: string, tag: ComputerFileEntityTag) => void;
|
|
25
48
|
/** Cancels the copy, which then removes its partial file before it rejects. */
|
|
26
49
|
signal?: AbortSignal;
|
|
27
50
|
/** Called after every chunk with the bytes copied so far and the file's size. */
|
|
@@ -35,8 +58,8 @@ export interface DedicatedCopyOptions extends Partial<DedicatedCopyIo> {
|
|
|
35
58
|
* (OSK-8714). Before, these local refusals had no code at all.
|
|
36
59
|
*/
|
|
37
60
|
export declare class DedicatedCopyError extends Error {
|
|
38
|
-
readonly code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED';
|
|
39
|
-
constructor(code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED', message: string);
|
|
61
|
+
readonly code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_PATH_IS_DIRECTORY' | 'DEDICATED_RUNTIME_FILE_EXISTS' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED';
|
|
62
|
+
constructor(code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_PATH_IS_DIRECTORY' | 'DEDICATED_RUNTIME_FILE_EXISTS' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED', message: string);
|
|
40
63
|
}
|
|
41
64
|
declare const SIGNAL_NUMBERS: {
|
|
42
65
|
readonly SIGHUP: 1;
|
|
@@ -76,6 +99,14 @@ export declare function listenForCopyInterrupt({ proc, notice, }?: {
|
|
|
76
99
|
export interface DedicatedCopyResult {
|
|
77
100
|
bytes: number;
|
|
78
101
|
destination: string;
|
|
102
|
+
/** Upload: true when the copy created the destination, false when it replaced one. */
|
|
103
|
+
created?: boolean;
|
|
104
|
+
/** Upload: the tag of the version that was replaced, as `stat` prints it. */
|
|
105
|
+
replacedTag?: string;
|
|
106
|
+
/** Upload: the committed file's tag, as `stat` prints it. */
|
|
107
|
+
tag?: string;
|
|
108
|
+
/** Upload: the machine answered from the receipt of an earlier, committed attempt. */
|
|
109
|
+
alreadyCommitted?: boolean;
|
|
79
110
|
}
|
|
80
111
|
/**
|
|
81
112
|
* One line of progress for a terminal, redrawn in place: a 60 MiB copy takes
|
|
@@ -100,13 +131,18 @@ export declare function intoRemoteDirectory(directory: string, name: string): st
|
|
|
100
131
|
* Upload one file. A destination that is a directory — spelled `dir/`, or an
|
|
101
132
|
* existing directory named without the slash — receives the file under its own
|
|
102
133
|
* name, as `cp` and `scp` do and as a download into a local directory already
|
|
103
|
-
* did
|
|
104
|
-
*
|
|
134
|
+
* did (OSK-8713).
|
|
135
|
+
*
|
|
136
|
+
* The destination is stat'ed before any byte is sent, and what the copy may do
|
|
137
|
+
* to it is decided then and enforced at commit by the machine (review CW-01):
|
|
105
138
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
139
|
+
* - nothing there → the copy creates it, and commits only if the name is still
|
|
140
|
+
* free (`createOnly`); something that appears meanwhile is never replaced;
|
|
141
|
+
* - a file there → refused before sending anything, unless `overwrite` (replace
|
|
142
|
+
* the version the stat found) or `ifTag` (replace exactly that version) was
|
|
143
|
+
* given; either way the commit replaces only that version (`replaceTag`), and
|
|
144
|
+
* a file changed during the copy is refused `file_changed`, nothing replaced.
|
|
109
145
|
*/
|
|
110
|
-
export declare function uploadToDedicatedRuntime(localPath: string, leaseId: string, remotePath: string, scope
|
|
111
|
-
export declare function downloadFromDedicatedRuntime(leaseId: string, remotePath: string, localPath: string, scope
|
|
146
|
+
export declare function uploadToDedicatedRuntime(localPath: string, leaseId: string, remotePath: string, scope: DedicatedRuntimeScope, io: DedicatedCopyOptions): Promise<DedicatedCopyResult>;
|
|
147
|
+
export declare function downloadFromDedicatedRuntime(leaseId: string, remotePath: string, localPath: string, scope: DedicatedRuntimeScope, io: DedicatedCopyOptions): Promise<DedicatedCopyResult>;
|
|
112
148
|
export {};
|
|
@@ -12,6 +12,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
12
12
|
const node_fs_1 = require("node:fs");
|
|
13
13
|
const promises_1 = require("node:fs/promises");
|
|
14
14
|
const node_path_1 = require("node:path");
|
|
15
|
+
const computer_files_1 = require("./computer-files");
|
|
15
16
|
const dedicated_file_retry_1 = require("./dedicated-file-retry");
|
|
16
17
|
const dedicated_machines_1 = require("./dedicated-machines");
|
|
17
18
|
/**
|
|
@@ -34,6 +35,7 @@ const changedWhileCopying = (path, where) => new DedicatedCopyError('DEDICATED_R
|
|
|
34
35
|
const defaultIo = {
|
|
35
36
|
readChunk: dedicated_machines_1.readDedicatedRuntimeFileChunk,
|
|
36
37
|
writeChunk: dedicated_machines_1.writeDedicatedRuntimeFileChunk,
|
|
38
|
+
statPath: computer_files_1.statComputerFile,
|
|
37
39
|
chunkBytes: dedicated_machines_1.DEDICATED_FILE_CHUNK_BYTES,
|
|
38
40
|
maxBytes: dedicated_machines_1.DEDICATED_FILE_MAX_BYTES,
|
|
39
41
|
onProcessExit: (fn) => {
|
|
@@ -138,51 +140,107 @@ function intoRemoteDirectory(directory, name) {
|
|
|
138
140
|
const parent = directory.replace(/\/\.?$/, '').replace(/^\.$/, '');
|
|
139
141
|
return parent ? `${parent}/${name}` : name;
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Stat one machine path for an upload. A path that does not exist is `absent`
|
|
145
|
+
* — the copy creates it, and any missing parent with it. Every other refusal
|
|
146
|
+
* (withheld, stale generation, the machine unreachable) is the copy's refusal.
|
|
147
|
+
*/
|
|
148
|
+
async function statDestination(statPath, leaseId, remotePath, generation, scope, signal) {
|
|
149
|
+
let result;
|
|
150
|
+
try {
|
|
151
|
+
result = await (0, dedicated_file_retry_1.withFileAdmissionRetry)(() => statPath(leaseId, { path: remotePath, generation }, scope), signal);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const refusal = (0, computer_files_1.computerFileRefusalInfo)(err);
|
|
155
|
+
// `details.path` (or a path-shaped message) marks a missing PATH; without
|
|
156
|
+
// it the machine itself was not found, which is not a destination to create.
|
|
157
|
+
if (refusal?.code === 'not_found' &&
|
|
158
|
+
(refusal.details?.path !== undefined || /path|file|folder|directory/i.test(refusal.message))) {
|
|
159
|
+
return { kind: 'absent' };
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
const type = result.entry.type;
|
|
164
|
+
if (type === 'directory')
|
|
165
|
+
return { kind: 'directory' };
|
|
166
|
+
if (type === 'file' && result.tag) {
|
|
167
|
+
return {
|
|
168
|
+
kind: 'file',
|
|
169
|
+
tag: { inode: result.tag.inode, size: result.tag.size, mtimeNs: result.tag.mtimeNs },
|
|
170
|
+
};
|
|
148
171
|
}
|
|
172
|
+
return { kind: 'other', type };
|
|
149
173
|
}
|
|
174
|
+
const sameTag = (a, b) => a.inode === b.inode && a.size === b.size && a.mtimeNs === b.mtimeNs;
|
|
150
175
|
/**
|
|
151
176
|
* Upload one file. A destination that is a directory — spelled `dir/`, or an
|
|
152
177
|
* existing directory named without the slash — receives the file under its own
|
|
153
178
|
* name, as `cp` and `scp` do and as a download into a local directory already
|
|
154
|
-
* did
|
|
155
|
-
* failed the machine's final rename with EISDIR (OSK-8713).
|
|
179
|
+
* did (OSK-8713).
|
|
156
180
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
181
|
+
* The destination is stat'ed before any byte is sent, and what the copy may do
|
|
182
|
+
* to it is decided then and enforced at commit by the machine (review CW-01):
|
|
183
|
+
*
|
|
184
|
+
* - nothing there → the copy creates it, and commits only if the name is still
|
|
185
|
+
* free (`createOnly`); something that appears meanwhile is never replaced;
|
|
186
|
+
* - a file there → refused before sending anything, unless `overwrite` (replace
|
|
187
|
+
* the version the stat found) or `ifTag` (replace exactly that version) was
|
|
188
|
+
* given; either way the commit replaces only that version (`replaceTag`), and
|
|
189
|
+
* a file changed during the copy is refused `file_changed`, nothing replaced.
|
|
160
190
|
*/
|
|
161
|
-
async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope, io) {
|
|
192
|
+
const { statPath, signal, generation } = { ...defaultIo, ...io };
|
|
193
|
+
throwIfCancelled(signal);
|
|
194
|
+
const info = await localSourceInfo(localPath, { ...defaultIo, ...io }.maxBytes);
|
|
195
|
+
const shown = (target) => `${leaseId}:${target}`;
|
|
196
|
+
let target = namesRemoteDirectory(remotePath)
|
|
197
|
+
? intoRemoteDirectory(remotePath, (0, node_path_1.basename)(localPath))
|
|
198
|
+
: remotePath;
|
|
199
|
+
let destination = await statDestination(statPath, leaseId, target, generation, scope, signal);
|
|
200
|
+
if (destination.kind === 'directory' && target === remotePath) {
|
|
201
|
+
// An existing directory named without the slash: into it, once.
|
|
202
|
+
target = intoRemoteDirectory(remotePath, (0, node_path_1.basename)(localPath));
|
|
203
|
+
destination = await statDestination(statPath, leaseId, target, generation, scope, signal);
|
|
204
|
+
}
|
|
205
|
+
if (destination.kind === 'directory') {
|
|
206
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_PATH_IS_DIRECTORY', `${shown(target)} is a directory on the machine; nothing was sent.`);
|
|
207
|
+
}
|
|
208
|
+
if (destination.kind === 'other') {
|
|
209
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_PATH_NOT_A_FILE', `${shown(target)} is a ${destination.type} on the machine, not a regular file; cp does not replace it. Nothing was sent.`);
|
|
210
|
+
}
|
|
211
|
+
let condition;
|
|
212
|
+
let replacedTag;
|
|
213
|
+
if (destination.kind === 'absent') {
|
|
214
|
+
if (io.ifTag) {
|
|
215
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${shown(target)} is no longer there, so the version --if-tag ${(0, computer_files_1.computerFileTagString)(io.ifTag)} names cannot be replaced. Nothing was sent; copy again without --if-tag to create it.`);
|
|
166
216
|
}
|
|
167
|
-
|
|
168
|
-
|
|
217
|
+
condition = { createOnly: true };
|
|
218
|
+
}
|
|
219
|
+
else if (io.ifTag) {
|
|
220
|
+
if (!sameTag(io.ifTag, destination.tag)) {
|
|
221
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${shown(target)} changed since --if-tag ${(0, computer_files_1.computerFileTagString)(io.ifTag)} was read; its current tag is ${(0, computer_files_1.computerFileTagString)(destination.tag)}. Nothing was sent or replaced.`);
|
|
169
222
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return attempt(intoDirectory(remotePath));
|
|
223
|
+
replacedTag = destination.tag;
|
|
224
|
+
condition = { replaceTag: destination.tag };
|
|
173
225
|
}
|
|
174
|
-
|
|
175
|
-
|
|
226
|
+
else if (io.overwrite) {
|
|
227
|
+
replacedTag = destination.tag;
|
|
228
|
+
condition = { replaceTag: destination.tag };
|
|
176
229
|
}
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
return attempt(intoDirectory(remotePath));
|
|
180
|
-
throw err;
|
|
230
|
+
else {
|
|
231
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_EXISTS', `${shown(target)} already exists (tag ${(0, computer_files_1.computerFileTagString)(destination.tag)}). Nothing was sent. Pass --overwrite to replace it, or --if-tag ${(0, computer_files_1.computerFileTagString)(destination.tag)} to replace exactly this version.`);
|
|
181
232
|
}
|
|
233
|
+
if (replacedTag)
|
|
234
|
+
io.onReplace?.(shown(target), replacedTag);
|
|
235
|
+
const result = await uploadFile(localPath, info, leaseId, target, scope, io, condition);
|
|
236
|
+
return {
|
|
237
|
+
...result,
|
|
238
|
+
created: replacedTag === undefined,
|
|
239
|
+
...(replacedTag ? { replacedTag: (0, computer_files_1.computerFileTagString)(replacedTag) } : {}),
|
|
240
|
+
};
|
|
182
241
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
throwIfCancelled(signal);
|
|
242
|
+
/** The local source of an upload: a regular file within the size limit. */
|
|
243
|
+
async function localSourceInfo(localPath, maxBytes) {
|
|
186
244
|
const info = await (0, promises_1.stat)(localPath).catch((err) => {
|
|
187
245
|
// The raw `ENOENT: no such file or directory, stat './x'` said the same
|
|
188
246
|
// thing in the platform's words rather than ours.
|
|
@@ -197,6 +255,34 @@ async function uploadFile(localPath, leaseId, remotePath, scope, io) {
|
|
|
197
255
|
if (info.size > maxBytes) {
|
|
198
256
|
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_TOO_LARGE', `${localPath} is ${mib(info.size)}; copy is limited to ${mib(maxBytes)}. Push it through Git or object storage instead.`);
|
|
199
257
|
}
|
|
258
|
+
return info;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The commit's own refusal of the condition the upload carried, in the copy's
|
|
262
|
+
* words: the destination changed (or appeared) DURING the copy and nothing was
|
|
263
|
+
* replaced. Anything else passes through unchanged.
|
|
264
|
+
*/
|
|
265
|
+
function commitConditionRefusal(err, shownTarget) {
|
|
266
|
+
const refusal = (0, computer_files_1.computerFileRefusalInfo)(err);
|
|
267
|
+
if (!refusal)
|
|
268
|
+
return err;
|
|
269
|
+
if (refusal.code === 'file_changed') {
|
|
270
|
+
let now = '';
|
|
271
|
+
if (refusal.currentTag !== undefined) {
|
|
272
|
+
now = ` Its current tag is ${(0, computer_files_1.computerFileTagString)(refusal.currentTag)}.`;
|
|
273
|
+
}
|
|
274
|
+
else if (refusal.body.currentTag === null) {
|
|
275
|
+
now = ' It is no longer there.';
|
|
276
|
+
}
|
|
277
|
+
return new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${shownTarget} changed on the machine during the copy, so nothing was replaced.${now} Look at it with \`stat\`, then copy again.`);
|
|
278
|
+
}
|
|
279
|
+
if (refusal.code === 'invalid_request' && refusal.details?.reason === 'exists') {
|
|
280
|
+
return new DedicatedCopyError('DEDICATED_RUNTIME_FILE_EXISTS', `${shownTarget} appeared on the machine during the copy, so nothing was replaced. Copy again with --overwrite if you mean to replace it.`);
|
|
281
|
+
}
|
|
282
|
+
return err;
|
|
283
|
+
}
|
|
284
|
+
async function uploadFile(localPath, info, leaseId, remotePath, scope, io, condition) {
|
|
285
|
+
const { writeChunk, chunkBytes, signal, onProgress, generation } = { ...defaultIo, ...io };
|
|
200
286
|
const transferId = `cp-${Date.now().toString(36)}-${(0, node_crypto_1.randomBytes)(6).toString('hex')}`;
|
|
201
287
|
const handle = await (0, promises_1.open)(localPath, 'r');
|
|
202
288
|
let offset = 0;
|
|
@@ -217,36 +303,45 @@ async function uploadFile(localPath, leaseId, remotePath, scope, io) {
|
|
|
217
303
|
path: remotePath,
|
|
218
304
|
transferId,
|
|
219
305
|
offset,
|
|
306
|
+
generation,
|
|
220
307
|
dataBase64: buffer.subarray(0, bytesRead).toString('base64'),
|
|
308
|
+
// On every chunk, so a machine whose image cannot honour the
|
|
309
|
+
// condition refuses the FIRST chunk, before anything is staged.
|
|
310
|
+
...condition,
|
|
221
311
|
// A file that GREW is caught here: the machine refuses to commit
|
|
222
312
|
// when what it staged is not the size declared at the start.
|
|
223
|
-
...(final
|
|
313
|
+
...(final
|
|
314
|
+
? { final: true, totalSize: info.size, executable: (info.mode & 0o111) !== 0 }
|
|
315
|
+
: {}),
|
|
224
316
|
}, scope), signal);
|
|
225
317
|
offset += bytesRead;
|
|
226
318
|
onProgress?.(offset, info.size);
|
|
227
|
-
|
|
228
|
-
|
|
319
|
+
// A receipt (`alreadyCommitted`) is the same success: this transfer
|
|
320
|
+
// committed earlier and only its acknowledgement was lost.
|
|
321
|
+
if (final || result.alreadyCommitted === true) {
|
|
322
|
+
return {
|
|
323
|
+
bytes: info.size,
|
|
324
|
+
destination: `${leaseId}:${result.path || remotePath}`,
|
|
325
|
+
...(result.tag ? { tag: (0, computer_files_1.computerFileTagString)(result.tag) } : {}),
|
|
326
|
+
...(result.alreadyCommitted === true ? { alreadyCommitted: true } : {}),
|
|
327
|
+
};
|
|
229
328
|
}
|
|
230
329
|
}
|
|
231
330
|
}
|
|
232
331
|
catch (err) {
|
|
233
|
-
if (offset === 0 && err?.code === PATH_IS_DIRECTORY) {
|
|
234
|
-
// Refused before the first chunk was staged: there is nothing to abort.
|
|
235
|
-
throw new DestinationIsDirectory(err);
|
|
236
|
-
}
|
|
237
332
|
// Best effort: remove the staged partial so it does not sit beside the
|
|
238
333
|
// destination. The destination itself was never touched. An abort that
|
|
239
334
|
// never arrives — the process killed, the network gone — is covered by the
|
|
240
335
|
// machine, which removes a stale staging file on the next upload into the
|
|
241
336
|
// same directory (daemon/src/tools/file-transfer.ts).
|
|
242
|
-
await writeChunk(leaseId, { path: remotePath, transferId, offset: 0, abort: true }, scope).catch(() => undefined);
|
|
243
|
-
throw err;
|
|
337
|
+
await writeChunk(leaseId, { path: remotePath, transferId, offset: 0, abort: true, generation }, scope).catch(() => undefined);
|
|
338
|
+
throw commitConditionRefusal(err, `${leaseId}:${remotePath}`);
|
|
244
339
|
}
|
|
245
340
|
finally {
|
|
246
341
|
await handle.close();
|
|
247
342
|
}
|
|
248
343
|
}
|
|
249
|
-
async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scope
|
|
344
|
+
async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scope, io) {
|
|
250
345
|
const { readChunk, chunkBytes, onProcessExit, signal, onProgress } = { ...defaultIo, ...io };
|
|
251
346
|
throwIfCancelled(signal);
|
|
252
347
|
let target = localPath;
|
|
@@ -282,14 +377,16 @@ async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scop
|
|
|
282
377
|
throwIfCancelled(signal);
|
|
283
378
|
// Nothing on the machine needs undoing, so an in-flight read is abandoned
|
|
284
379
|
// at once rather than waited out.
|
|
285
|
-
const chunk = await (0, dedicated_file_retry_1.withFileAdmissionRetry)(() => readChunk(leaseId, { path: remotePath, offset, length: chunkBytes }, scope, signal), signal);
|
|
380
|
+
const chunk = await (0, dedicated_file_retry_1.withFileAdmissionRetry)(() => readChunk(leaseId, { path: remotePath, offset, length: chunkBytes, generation: io.generation }, scope, signal), signal);
|
|
286
381
|
if (size === -1) {
|
|
287
382
|
size = chunk.size;
|
|
288
383
|
mtimeMs = chunk.mtimeMs;
|
|
289
384
|
executable = chunk.executable;
|
|
290
385
|
}
|
|
291
|
-
else if (chunk.size !== size ||
|
|
292
|
-
|
|
386
|
+
else if (chunk.size !== size ||
|
|
387
|
+
chunk.mtimeMs !== mtimeMs ||
|
|
388
|
+
(typeof chunk.executable === 'boolean' &&
|
|
389
|
+
typeof executable === 'boolean' &&
|
|
293
390
|
chunk.executable !== executable)) {
|
|
294
391
|
// Chunks from two versions of a file splice into one that never existed.
|
|
295
392
|
throw changedWhileCopying(remotePath, 'on the machine');
|
|
@@ -285,6 +285,7 @@ export declare function dedicatedRuntimeCreateInput(input: {
|
|
|
285
285
|
* billable request is sent.
|
|
286
286
|
*/
|
|
287
287
|
export declare function isDedicatedRepositoryRef(value: string): boolean;
|
|
288
|
+
export declare function withDedicatedScope(path: string, scope: DedicatedRuntimeScope, query?: Record<string, string>): string;
|
|
288
289
|
export declare function createDedicatedRuntime(input: CreateDedicatedRuntimeInput): Promise<DedicatedRuntimeResponse>;
|
|
289
290
|
/** One offered tier as the operator price book checks it. */
|
|
290
291
|
export interface DedicatedPriceBookPresetCheck {
|
|
@@ -357,6 +358,34 @@ export declare function restoreDeletedDedicatedRuntime(leaseId: string, options?
|
|
|
357
358
|
* on the next reconciler pass.
|
|
358
359
|
*/
|
|
359
360
|
export declare function setDedicatedRuntimeSpendingLimit(leaseId: string, spendingLimitCents: number | null, scope?: DedicatedRuntimeScope): Promise<DedicatedRuntimeResponse>;
|
|
361
|
+
/**
|
|
362
|
+
* Whether the agents working on a Dedicated Runtime get a browser (OSK-13809).
|
|
363
|
+
*
|
|
364
|
+
* `state` is the machine's setting; `offered` is whether its CONNECTED daemon
|
|
365
|
+
* offers the browser right now (null: nothing connected, or unreadable). The
|
|
366
|
+
* two differ after a change until the daemon reconnects, and on an image
|
|
367
|
+
* without Chrome, which never offers it.
|
|
368
|
+
*/
|
|
369
|
+
export type DedicatedAgentBrowserState = {
|
|
370
|
+
leaseId: string;
|
|
371
|
+
state: 'on' | 'off' | 'pending_first_start';
|
|
372
|
+
enabled: boolean;
|
|
373
|
+
offered: boolean | null;
|
|
374
|
+
capability: string;
|
|
375
|
+
changed?: boolean;
|
|
376
|
+
effectiveOnNextReconnect?: boolean;
|
|
377
|
+
};
|
|
378
|
+
export declare function getDedicatedRuntimeAgentBrowser(leaseId: string, scope?: DedicatedRuntimeScope): Promise<DedicatedAgentBrowserState>;
|
|
379
|
+
/** Turn the agents' browser on or off. An actual change is in the machine's audit record. */
|
|
380
|
+
export declare function setDedicatedRuntimeAgentBrowser(leaseId: string, enabled: boolean, scope?: DedicatedRuntimeScope): Promise<DedicatedAgentBrowserState>;
|
|
381
|
+
/**
|
|
382
|
+
* One line for a person. Says the bad cases out loud: a setting that is on
|
|
383
|
+
* while the connected daemon does not offer it yet, and a setting we could
|
|
384
|
+
* not read at all.
|
|
385
|
+
*/
|
|
386
|
+
export declare function describeDedicatedAgentBrowser(value: DedicatedAgentBrowserState | {
|
|
387
|
+
error: string;
|
|
388
|
+
} | null | undefined, leaseId: string): string;
|
|
360
389
|
export declare function closeDedicatedRuntimeIncidentHold(leaseId: string, options?: {
|
|
361
390
|
requestId?: string;
|
|
362
391
|
} & DedicatedRuntimeScope): Promise<DedicatedRuntimeResponse>;
|
|
@@ -478,21 +507,69 @@ export interface DedicatedFileWriteResult {
|
|
|
478
507
|
staged: number;
|
|
479
508
|
committed: boolean;
|
|
480
509
|
aborted?: boolean;
|
|
510
|
+
/** On commit: the committed file's entity tag. */
|
|
511
|
+
tag?: {
|
|
512
|
+
inode: string;
|
|
513
|
+
size: number;
|
|
514
|
+
mtimeNs: string;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* This transfer had ALREADY committed (review CW-02): the answer is its
|
|
518
|
+
* receipt, and nothing was written again — a retried final chunk whose first
|
|
519
|
+
* acknowledgement was lost. A success, never a reason to upload again.
|
|
520
|
+
*/
|
|
521
|
+
alreadyCommitted?: boolean;
|
|
481
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* `lease:<leaseId>` is the machine id the computer file routes address a
|
|
525
|
+
* Dedicated Runtime by (`/api/machines/:machineId/files/*`, authorised by
|
|
526
|
+
* ComputerAccess), the same id the machine inventory uses.
|
|
527
|
+
*/
|
|
528
|
+
export declare const computerMachineId: (leaseId: string) => string;
|
|
529
|
+
/**
|
|
530
|
+
* The computer's page for a Dedicated Runtime — `/machines/<machineId>/computer`
|
|
531
|
+
* (the route `paneRouteConfig` serves the computer shell at, design §11).
|
|
532
|
+
* Composed on the `machine` web route rather than concatenated against baseURL,
|
|
533
|
+
* so the `api.`-subdomain → apex origin mapping stays the one in web-url.ts.
|
|
534
|
+
*/
|
|
535
|
+
export declare function dedicatedComputerUrl(baseURL: string, leaseId: string): string | null;
|
|
536
|
+
/**
|
|
537
|
+
* One chunk of a download. `generation` is the lease generation the `cp`
|
|
538
|
+
* command resolved at its start, presented on every chunk (review C-01): a
|
|
539
|
+
* machine replaced mid-copy refuses the rest `stale_generation`.
|
|
540
|
+
*/
|
|
482
541
|
export declare function readDedicatedRuntimeFileChunk(leaseId: string, input: {
|
|
483
542
|
path: string;
|
|
484
543
|
offset: number;
|
|
485
544
|
length: number;
|
|
545
|
+
generation: number;
|
|
486
546
|
}, scope?: DedicatedRuntimeScope, signal?: AbortSignal): Promise<DedicatedFileChunk>;
|
|
547
|
+
/**
|
|
548
|
+
* One call of a chunked upload. `generation` fences every chunk to the machine
|
|
549
|
+
* the command resolved (review C-01). How the commit treats a destination is
|
|
550
|
+
* the caller's explicit choice (review CW-01), never an unconditional replace:
|
|
551
|
+
* `createOnly` commits only if nothing has the name (else 409
|
|
552
|
+
* `invalid_request`/`exists`); `replaceTag` commits only over the file with
|
|
553
|
+
* exactly that tag (else 409 `file_changed` with `currentTag`, null when it is
|
|
554
|
+
* gone). Never both. Each needs `computer_files_fenced_v1` on the machine.
|
|
555
|
+
*/
|
|
487
556
|
export declare function writeDedicatedRuntimeFileChunk(leaseId: string, input: {
|
|
488
557
|
path: string;
|
|
489
558
|
transferId: string;
|
|
490
559
|
offset: number;
|
|
560
|
+
generation: number;
|
|
491
561
|
dataBase64?: string;
|
|
492
562
|
final?: boolean;
|
|
493
563
|
abort?: boolean;
|
|
564
|
+
query?: boolean;
|
|
494
565
|
totalSize?: number;
|
|
495
566
|
executable?: boolean;
|
|
567
|
+
createOnly?: boolean;
|
|
568
|
+
replaceTag?: {
|
|
569
|
+
inode: string;
|
|
570
|
+
size: number;
|
|
571
|
+
mtimeNs: string;
|
|
572
|
+
};
|
|
496
573
|
}, scope?: DedicatedRuntimeScope): Promise<DedicatedFileWriteResult>;
|
|
497
574
|
export type DedicatedCopyPlan = {
|
|
498
575
|
direction: 'upload';
|
|
@@ -578,6 +655,9 @@ export declare function dedicatedSpendSummary(lease: DedicatedLease): string;
|
|
|
578
655
|
export declare function describeDedicatedDeletion(deletion: DedicatedLeaseDeletion, leaseId?: string): string;
|
|
579
656
|
export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[], opts?: {
|
|
580
657
|
latestDaemonRelease?: LatestDaemonRelease | null;
|
|
658
|
+
agentBrowser?: DedicatedAgentBrowserState | {
|
|
659
|
+
error: string;
|
|
660
|
+
} | null;
|
|
581
661
|
}): string[];
|
|
582
662
|
/** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
|
|
583
663
|
export declare function formatDedicatedDeadline(value: string | null | undefined, now?: Date): string;
|