@orion-agents/orion-code 0.3.11 → 0.3.14
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/CHANGELOG.md +94 -2
- package/README.md +8 -5
- package/README.zh-CN.md +5 -4
- package/dist/cli.js +14 -1
- package/dist/web/context-text.d.ts +17 -0
- package/dist/web/context-text.js +40 -0
- package/dist/web/file-read-service.d.ts +35 -0
- package/dist/web/file-read-service.js +158 -1
- package/dist/web/file-write-service.d.ts +16 -0
- package/dist/web/file-write-service.js +103 -0
- package/dist/web/git-read-model-service.d.ts +14 -0
- package/dist/web/git-read-model-service.js +64 -0
- package/dist/web/host-daemon.d.ts +9 -0
- package/dist/web/host-daemon.js +45 -1
- package/dist/web/protocol.d.ts +9 -2
- package/dist/web/protocol.js +26 -0
- package/dist/web/review-service.d.ts +2 -19
- package/dist/web/review-service.js +4 -39
- package/dist/web/server.js +111 -0
- package/dist/web/workbench-controller.d.ts +47 -0
- package/dist/web/workbench-controller.js +93 -13
- package/dist/web-client/assets/{DiffViewer-BrE5tfZc.js → DiffViewer-BrHqudpK.js} +1 -1
- package/dist/web-client/assets/FilesPanel-BcFY6L1a.js +1 -0
- package/dist/web-client/assets/GitPanel-B6E5IEXm.js +1 -0
- package/dist/web-client/assets/ResourceSplitLayout-fPYE9JKJ.js +1 -0
- package/dist/web-client/assets/ReviewPanel-5ecTVmhT.js +1 -0
- package/dist/web-client/assets/{TerminalPanel-CMLU7j08.js → TerminalPanel-IkCttxgw.js} +1 -1
- package/dist/web-client/assets/index-DgVNIqmE.js +20 -0
- package/dist/web-client/assets/index-R6E589Fc.css +1 -0
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/web-client/assets/FilesPanel-ZwlVgxJi.js +0 -2
- package/dist/web-client/assets/GitPanel-Cnz52VRO.js +0 -1
- package/dist/web-client/assets/ReviewPanel-D_xxkVG_.js +0 -1
- package/dist/web-client/assets/index-BlFUf8dG.js +0 -17
- package/dist/web-client/assets/index-CUG6w669.css +0 -1
|
@@ -262,6 +262,56 @@ class GitReadModelServiceV1 {
|
|
|
262
262
|
this.pathById.set(id, path);
|
|
263
263
|
return id;
|
|
264
264
|
}
|
|
265
|
+
/** v0.3.12 S3 — guarded stage: paths are host-resolved file ids only. */
|
|
266
|
+
async stagePaths(paths) {
|
|
267
|
+
assertSafeGitPaths(paths);
|
|
268
|
+
if (paths.length === 0)
|
|
269
|
+
throw new Error('stage requires at least one path.');
|
|
270
|
+
await this.mutate(['add', '--', ...paths]);
|
|
271
|
+
return { repositoryRevision: await this.revisionAfterMutation() };
|
|
272
|
+
}
|
|
273
|
+
async unstagePaths(paths) {
|
|
274
|
+
assertSafeGitPaths(paths);
|
|
275
|
+
if (paths.length === 0)
|
|
276
|
+
throw new Error('unstage requires at least one path.');
|
|
277
|
+
await this.mutate(['reset', '-q', '--', ...paths]);
|
|
278
|
+
return { repositoryRevision: await this.revisionAfterMutation() };
|
|
279
|
+
}
|
|
280
|
+
async commit(message) {
|
|
281
|
+
const trimmed = message.trim();
|
|
282
|
+
if (!trimmed || trimmed.length > 2000 || /[\x00-\x1f]/.test(trimmed)) {
|
|
283
|
+
throw new Error('A valid commit message is required.');
|
|
284
|
+
}
|
|
285
|
+
const root = await this.requireRepositoryRoot();
|
|
286
|
+
const porcelain = (await this.runGit(['status', '--porcelain=v1'], root)).trim();
|
|
287
|
+
const stagedLines = porcelain
|
|
288
|
+
.split('\n')
|
|
289
|
+
.filter(line => line.length > 1 && line[0] !== ' ' && line[0] !== '?' && line[0] !== '!');
|
|
290
|
+
if (stagedLines.length === 0) {
|
|
291
|
+
throw new Error('Nothing is staged to commit.');
|
|
292
|
+
}
|
|
293
|
+
await this.mutate(['commit', '-m', trimmed]);
|
|
294
|
+
const sha = (await this.runGit(['rev-parse', 'HEAD'], root)).trim();
|
|
295
|
+
return { repositoryRevision: await this.revisionAfterMutation(), commitSha: sha };
|
|
296
|
+
}
|
|
297
|
+
async mutate(args) {
|
|
298
|
+
const root = await this.requireRepositoryRoot();
|
|
299
|
+
const output = await this.runGit(args, root);
|
|
300
|
+
if (output) {
|
|
301
|
+
// Warnings on stderr are not captured by runGit; treat non-empty as unexpected.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async requireRepositoryRoot() {
|
|
305
|
+
const snapshot = await this.capture();
|
|
306
|
+
if (!snapshot.isRepository || !snapshot.root) {
|
|
307
|
+
throw new Error('Git repository is unavailable.');
|
|
308
|
+
}
|
|
309
|
+
return snapshot.root;
|
|
310
|
+
}
|
|
311
|
+
async revisionAfterMutation() {
|
|
312
|
+
const root = await this.requireRepositoryRoot();
|
|
313
|
+
return this.runGit(['rev-parse', 'HEAD'], root).then(value => value.trim());
|
|
314
|
+
}
|
|
265
315
|
runGit(args, cwd) {
|
|
266
316
|
return new Promise((resolvePromise, reject) => {
|
|
267
317
|
this.performance.processCount += 1;
|
|
@@ -590,6 +640,20 @@ function streamGitLines(input) {
|
|
|
590
640
|
});
|
|
591
641
|
});
|
|
592
642
|
}
|
|
643
|
+
function assertSafeGitPaths(paths) {
|
|
644
|
+
for (const path of paths) {
|
|
645
|
+
if (typeof path !== 'string' ||
|
|
646
|
+
path.length === 0 ||
|
|
647
|
+
path.length > 4096 ||
|
|
648
|
+
path.includes('\0') ||
|
|
649
|
+
path.startsWith('-') ||
|
|
650
|
+
path.includes('/../') ||
|
|
651
|
+
path === '..' ||
|
|
652
|
+
path.startsWith('../')) {
|
|
653
|
+
throw new Error(`Unsafe Git path rejected: ${JSON.stringify(path)}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
593
657
|
function gitEnvironment() {
|
|
594
658
|
return {
|
|
595
659
|
PATH: process.env.PATH,
|
|
@@ -48,6 +48,15 @@ export declare function hostDaemonStatus(port: number): HostDaemonStatus;
|
|
|
48
48
|
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
49
49
|
* foreground command can print the URL and exit cleanly.
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* True when something already listens on 127.0.0.1:port — typically another
|
|
53
|
+
* Orion host process (e.g. one managed by launchd, which does not write a
|
|
54
|
+
* pidfile). Detecting this up front turns a raw EADDRINUSE crash into a
|
|
55
|
+
* friendly message.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isHostPortTaken(port: number): Promise<boolean>;
|
|
58
|
+
/** Best-effort tail of the host log used when a background child exits early. */
|
|
59
|
+
export declare function readHostLogTail(port: number, lines?: number): string;
|
|
51
60
|
export declare function spawnBackgroundHost(options: {
|
|
52
61
|
readonly cwd: string;
|
|
53
62
|
readonly port: number;
|
package/dist/web/host-daemon.js
CHANGED
|
@@ -9,6 +9,8 @@ exports.writeHostPidfile = writeHostPidfile;
|
|
|
9
9
|
exports.clearHostPidfile = clearHostPidfile;
|
|
10
10
|
exports.isProcessAlive = isProcessAlive;
|
|
11
11
|
exports.hostDaemonStatus = hostDaemonStatus;
|
|
12
|
+
exports.isHostPortTaken = isHostPortTaken;
|
|
13
|
+
exports.readHostLogTail = readHostLogTail;
|
|
12
14
|
exports.spawnBackgroundHost = spawnBackgroundHost;
|
|
13
15
|
exports.stopBackgroundHost = stopBackgroundHost;
|
|
14
16
|
/**
|
|
@@ -26,6 +28,7 @@ exports.stopBackgroundHost = stopBackgroundHost;
|
|
|
26
28
|
* "startedAt": 1725000000000, "workspace": "/abs/path" }
|
|
27
29
|
*/
|
|
28
30
|
const child_process_1 = require("child_process");
|
|
31
|
+
const net_1 = require("net");
|
|
29
32
|
const fs_1 = require("fs");
|
|
30
33
|
const path_1 = require("path");
|
|
31
34
|
const paths_1 = require("../product/paths");
|
|
@@ -119,7 +122,48 @@ function childArgs(webArgs) {
|
|
|
119
122
|
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
120
123
|
* foreground command can print the URL and exit cleanly.
|
|
121
124
|
*/
|
|
122
|
-
|
|
125
|
+
/**
|
|
126
|
+
* True when something already listens on 127.0.0.1:port — typically another
|
|
127
|
+
* Orion host process (e.g. one managed by launchd, which does not write a
|
|
128
|
+
* pidfile). Detecting this up front turns a raw EADDRINUSE crash into a
|
|
129
|
+
* friendly message.
|
|
130
|
+
*/
|
|
131
|
+
function isHostPortTaken(port) {
|
|
132
|
+
return new Promise(resolve => {
|
|
133
|
+
const socket = (0, net_1.connect)({ host: '127.0.0.1', port });
|
|
134
|
+
let settled = false;
|
|
135
|
+
const done = (taken) => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
socket.destroy();
|
|
140
|
+
resolve(taken);
|
|
141
|
+
};
|
|
142
|
+
socket.once('connect', () => done(true));
|
|
143
|
+
socket.once('error', () => done(false));
|
|
144
|
+
socket.setTimeout(800, () => done(false));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Best-effort tail of the host log used when a background child exits early. */
|
|
148
|
+
function readHostLogTail(port, lines = 6) {
|
|
149
|
+
try {
|
|
150
|
+
const raw = (0, fs_1.readFileSync)((0, path_1.join)(hostLogsDirectory(), `web-${port}.log`), 'utf8');
|
|
151
|
+
return raw.split('\n').filter(Boolean).slice(-lines).join('\n');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function assertHostPortAvailable(port) {
|
|
158
|
+
if (await isHostPortTaken(port)) {
|
|
159
|
+
throw new Error(`Port ${port} is already served by another Orion host process. This is ` +
|
|
160
|
+
`often a launchd LaunchAgent (ai.orion-code.web). Check with ` +
|
|
161
|
+
'`launchctl list | grep orion`; if the running host belongs to this ' +
|
|
162
|
+
`CLI, use \`orion web status --port ${port}\` to confirm before stopping it.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function spawnBackgroundHost(options) {
|
|
166
|
+
await assertHostPortAvailable(options.port);
|
|
123
167
|
const logsDirectory = hostLogsDirectory();
|
|
124
168
|
(0, fs_1.mkdirSync)(logsDirectory, { recursive: true });
|
|
125
169
|
const logPath = (0, path_1.join)(logsDirectory, `web-${options.port}.log`);
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { SessionMeta } from '../services/session-storage';
|
|
|
9
9
|
import type { ToolConfirmationPolicy } from '../services/global-config';
|
|
10
10
|
import type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1 } from './file-read-service';
|
|
11
11
|
import type { WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1 } from './git-read-model-service';
|
|
12
|
-
import type { WebReviewSnapshotV1
|
|
12
|
+
import type { WebReviewSnapshotV1 } from './review-service';
|
|
13
13
|
import type { WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
|
|
14
14
|
import type { WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1 } from './terminal-manager';
|
|
15
15
|
export declare const WEB_API_VERSION: 1;
|
|
@@ -171,6 +171,13 @@ export type WebContextReferenceV1 = {
|
|
|
171
171
|
readonly id: string;
|
|
172
172
|
readonly label: string;
|
|
173
173
|
readonly revision: string;
|
|
174
|
+
} | {
|
|
175
|
+
readonly kind: 'file_range';
|
|
176
|
+
readonly id: string;
|
|
177
|
+
readonly label: string;
|
|
178
|
+
readonly revision: string;
|
|
179
|
+
readonly startLine: number;
|
|
180
|
+
readonly endLine: number;
|
|
174
181
|
} | {
|
|
175
182
|
readonly kind: 'folder';
|
|
176
183
|
readonly id: string;
|
|
@@ -369,7 +376,7 @@ export type WebWorkspaceResourceV1 = 'files' | 'git' | 'review';
|
|
|
369
376
|
export type WebWorkspaceInvalidationReasonV1 = 'context-change' | 'filesystem-change' | 'terminal-command' | 'tool-finished';
|
|
370
377
|
export type WebToolDetailSummaryV1 = ToolDetailSummary;
|
|
371
378
|
export type WebToolDetailPageV1 = ToolDetailPage;
|
|
372
|
-
export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1, WebReviewSnapshotV1,
|
|
379
|
+
export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusV1, WebReviewSnapshotV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
|
|
373
380
|
export interface WebBootstrapV1 {
|
|
374
381
|
readonly apiVersion: 1;
|
|
375
382
|
readonly productVersion: string;
|
package/dist/web/protocol.js
CHANGED
|
@@ -523,6 +523,7 @@ function parseContextReferences(value) {
|
|
|
523
523
|
const row = requireRecord(entry, `contextReferences[${index}]`);
|
|
524
524
|
const kind = requireEnum(row.kind, `contextReferences[${index}].kind`, [
|
|
525
525
|
'file',
|
|
526
|
+
'file_range',
|
|
526
527
|
'folder',
|
|
527
528
|
'review',
|
|
528
529
|
'session',
|
|
@@ -534,6 +535,22 @@ function parseContextReferences(value) {
|
|
|
534
535
|
}
|
|
535
536
|
seen.add(`${kind}:${id}`);
|
|
536
537
|
const label = requireBoundedString(row.label, `contextReferences[${index}].label`, 200);
|
|
538
|
+
if (kind === 'file_range') {
|
|
539
|
+
assertOnlyKeys(row, ['kind', 'id', 'label', 'revision', 'startLine', 'endLine'], 'Context reference');
|
|
540
|
+
const startLine = requireLineNumber(row.startLine, `contextReferences[${index}].startLine`);
|
|
541
|
+
const endLine = requireLineNumber(row.endLine, `contextReferences[${index}].endLine`);
|
|
542
|
+
if (endLine < startLine) {
|
|
543
|
+
throw new WebProtocolError('file_range endLine must not precede startLine');
|
|
544
|
+
}
|
|
545
|
+
return Object.freeze({
|
|
546
|
+
kind,
|
|
547
|
+
id,
|
|
548
|
+
label,
|
|
549
|
+
revision: requireBoundedString(row.revision, `contextReferences[${index}].revision`, 256),
|
|
550
|
+
startLine,
|
|
551
|
+
endLine,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
537
554
|
if (kind === 'file' || kind === 'folder') {
|
|
538
555
|
assertOnlyKeys(row, ['kind', 'id', 'label', 'revision'], 'Context reference');
|
|
539
556
|
return Object.freeze({
|
|
@@ -573,6 +590,15 @@ function assertOnlyKeys(row, keys, subject = 'command') {
|
|
|
573
590
|
if (unknown)
|
|
574
591
|
throw new WebProtocolError(`Unknown ${subject} field: ${unknown}`);
|
|
575
592
|
}
|
|
593
|
+
function requireLineNumber(value, name) {
|
|
594
|
+
if (typeof value !== 'number' ||
|
|
595
|
+
!Number.isSafeInteger(value) ||
|
|
596
|
+
value < 1 ||
|
|
597
|
+
value > 100000000) {
|
|
598
|
+
throw new WebProtocolError(`${name} must be an integer line number from 1 through 100000000`);
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
576
602
|
function requireBoundedString(value, name, maxLength) {
|
|
577
603
|
if (typeof value !== 'string' || !value.trim()) {
|
|
578
604
|
throw new WebProtocolError(`${name} must be a non-empty string`);
|
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
import type { VerifiedDurableToolReceiptRefV1 } from '../runtime/durable-tool-receipt-reader';
|
|
2
1
|
import type { GitReadModelServiceV1, WebGitDiffPageV1, WebGitFileV1 } from './git-read-model-service';
|
|
3
|
-
export interface WebReviewVerificationV1 {
|
|
4
|
-
readonly callId: string;
|
|
5
|
-
readonly sessionId: string;
|
|
6
|
-
readonly threadId: string;
|
|
7
|
-
readonly sequence: number;
|
|
8
|
-
readonly toolName: string;
|
|
9
|
-
readonly state: 'success' | 'error' | 'skipped';
|
|
10
|
-
readonly terminal: VerifiedDurableToolReceiptRefV1['terminal'];
|
|
11
|
-
readonly success: boolean;
|
|
12
|
-
readonly outputBytes: number;
|
|
13
|
-
readonly hasArtifact: boolean;
|
|
14
|
-
readonly executionPolicyDigest: string;
|
|
15
|
-
readonly receiptDigest: string;
|
|
16
|
-
}
|
|
17
2
|
export interface WebReviewSnapshotV1 {
|
|
18
3
|
readonly revision: string;
|
|
19
4
|
readonly repositoryRevision: string;
|
|
@@ -26,13 +11,11 @@ export interface WebReviewSnapshotV1 {
|
|
|
26
11
|
readonly untrackedCount: number;
|
|
27
12
|
readonly conflictCount: number;
|
|
28
13
|
readonly truncated: boolean;
|
|
29
|
-
readonly verification: readonly WebReviewVerificationV1[];
|
|
30
14
|
}
|
|
31
|
-
/** Review overview composed only from Git facts
|
|
15
|
+
/** Review overview composed only from Git facts. */
|
|
32
16
|
export declare class ReviewServiceV1 {
|
|
33
17
|
private readonly git;
|
|
34
|
-
|
|
35
|
-
constructor(git: GitReadModelServiceV1, listReceiptRefs: () => Promise<readonly VerifiedDurableToolReceiptRefV1[]> | readonly VerifiedDurableToolReceiptRefV1[]);
|
|
18
|
+
constructor(git: GitReadModelServiceV1);
|
|
36
19
|
snapshot(): Promise<WebReviewSnapshotV1>;
|
|
37
20
|
diff(input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<WebGitDiffPageV1>;
|
|
38
21
|
}
|
|
@@ -2,21 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ReviewServiceV1 = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
|
-
/** Review overview composed only from Git facts
|
|
5
|
+
/** Review overview composed only from Git facts. */
|
|
6
6
|
class ReviewServiceV1 {
|
|
7
|
-
constructor(git
|
|
7
|
+
constructor(git) {
|
|
8
8
|
this.git = git;
|
|
9
|
-
this.listReceiptRefs = listReceiptRefs;
|
|
10
9
|
}
|
|
11
10
|
async snapshot() {
|
|
12
11
|
// v0.3.9 #237/#228 — collect every Git status page. A repository with more
|
|
13
12
|
// than the single pageSize of changed files was silently truncated before;
|
|
14
13
|
// the review overview must aggregate all pages (bounded by a guard that
|
|
15
14
|
// keeps runaway repositories from looping forever).
|
|
16
|
-
const
|
|
17
|
-
this.listReceiptRefs(),
|
|
18
|
-
collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) })),
|
|
19
|
-
]);
|
|
15
|
+
const statusPages = await collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) }));
|
|
20
16
|
const status = statusPages.at(-1) ?? (await this.git.status({ pageSize: 2000 }));
|
|
21
17
|
const conflicted = [];
|
|
22
18
|
const staged = [];
|
|
@@ -29,25 +25,8 @@ class ReviewServiceV1 {
|
|
|
29
25
|
untracked.push(...page.untracked);
|
|
30
26
|
}
|
|
31
27
|
const changedFiles = uniqueFiles([...conflicted, ...staged, ...unstaged, ...untracked]);
|
|
32
|
-
const verification = receiptRefs.slice(0, 100).map(receipt => Object.freeze({
|
|
33
|
-
callId: receipt.callId,
|
|
34
|
-
sessionId: receipt.sessionId,
|
|
35
|
-
threadId: receipt.threadId,
|
|
36
|
-
sequence: receipt.sequence,
|
|
37
|
-
toolName: receipt.toolName,
|
|
38
|
-
state: reviewState(receipt),
|
|
39
|
-
terminal: receipt.terminal,
|
|
40
|
-
success: receipt.success,
|
|
41
|
-
outputBytes: receipt.outputBytes,
|
|
42
|
-
hasArtifact: receipt.hasArtifact,
|
|
43
|
-
executionPolicyDigest: receipt.executionPolicyDigest,
|
|
44
|
-
receiptDigest: receipt.receiptDigest,
|
|
45
|
-
}));
|
|
46
28
|
const revision = (0, crypto_1.createHash)('sha256')
|
|
47
|
-
.update(JSON.stringify({
|
|
48
|
-
repositoryRevision: status.repositoryRevision,
|
|
49
|
-
verification,
|
|
50
|
-
}))
|
|
29
|
+
.update(JSON.stringify({ repositoryRevision: status.repositoryRevision }))
|
|
51
30
|
.digest('hex');
|
|
52
31
|
return Object.freeze({
|
|
53
32
|
revision,
|
|
@@ -61,7 +40,6 @@ class ReviewServiceV1 {
|
|
|
61
40
|
untrackedCount: untracked.length,
|
|
62
41
|
conflictCount: conflicted.length,
|
|
63
42
|
truncated: statusPages.at(-1)?.truncated ?? false,
|
|
64
|
-
verification: Object.freeze(verification),
|
|
65
43
|
});
|
|
66
44
|
}
|
|
67
45
|
diff(input) {
|
|
@@ -69,25 +47,12 @@ class ReviewServiceV1 {
|
|
|
69
47
|
}
|
|
70
48
|
}
|
|
71
49
|
exports.ReviewServiceV1 = ReviewServiceV1;
|
|
72
|
-
function reviewState(receipt) {
|
|
73
|
-
if (receipt.success)
|
|
74
|
-
return 'success';
|
|
75
|
-
return receipt.terminal === 'interrupted' || receipt.terminal === 'indeterminate'
|
|
76
|
-
? 'skipped'
|
|
77
|
-
: 'error';
|
|
78
|
-
}
|
|
79
50
|
function uniqueFiles(files) {
|
|
80
51
|
const byId = new Map();
|
|
81
52
|
for (const file of files)
|
|
82
53
|
byId.set(file.fileId, file);
|
|
83
54
|
return [...byId.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
84
55
|
}
|
|
85
|
-
/**
|
|
86
|
-
* v0.3.9 #237/#228 — page through Git status until the snapshot is fully
|
|
87
|
-
* collected. The bounded guard (64 pages x 2_000 = 128k files) exists to stop
|
|
88
|
-
* runaway repositories from looping; a repository exceeding it stays marked
|
|
89
|
-
* truncated so the UI can still communicate the limit.
|
|
90
|
-
*/
|
|
91
56
|
async function collectStatusPages(fetchPage) {
|
|
92
57
|
const pages = [];
|
|
93
58
|
let page = await fetchPage(undefined);
|
package/dist/web/server.js
CHANGED
|
@@ -387,6 +387,15 @@ async function handleRequest(context) {
|
|
|
387
387
|
sendJson(response, 200, collectionPage(url, 'mcp', context.workbench.mcp(contextGuard), server => server.id));
|
|
388
388
|
return;
|
|
389
389
|
}
|
|
390
|
+
if (method === 'GET' && path === '/files/search') {
|
|
391
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
392
|
+
const query = url.searchParams.get('q') ?? '';
|
|
393
|
+
const scopeValue = url.searchParams.get('scope');
|
|
394
|
+
const scope = scopeValue === 'content' ? 'content' : 'name';
|
|
395
|
+
const limit = boundedInteger(url.searchParams.get('limit'), 50, 1, 200);
|
|
396
|
+
sendJson(response, 200, context.workbench.searchFiles(contextGuard, { query, scope, limit }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
390
399
|
if (method === 'GET' && path === '/files') {
|
|
391
400
|
const contextGuard = requireContextGuardQuery(url);
|
|
392
401
|
sendJson(response, 200, context.workbench.listFiles(contextGuard, {
|
|
@@ -422,6 +431,73 @@ async function handleRequest(context) {
|
|
|
422
431
|
}));
|
|
423
432
|
return;
|
|
424
433
|
}
|
|
434
|
+
const fileWriteMatch = path === '/files/write';
|
|
435
|
+
if (method === 'POST' && fileWriteMatch) {
|
|
436
|
+
assertMutation(request, context.nonce, context.origin, 'file_mutation_forbidden');
|
|
437
|
+
assertFileUserGesture(request);
|
|
438
|
+
const body = requireRecord(await readJson(request), 'File write request');
|
|
439
|
+
assertOnlyKeys(body, [
|
|
440
|
+
'requestId',
|
|
441
|
+
'fileId',
|
|
442
|
+
'content',
|
|
443
|
+
'expectedRevision',
|
|
444
|
+
'workspaceId',
|
|
445
|
+
'expectedContextRevision',
|
|
446
|
+
]);
|
|
447
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
448
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
449
|
+
const fileId = requireText(body.fileId, 'fileId', 256);
|
|
450
|
+
const content = requireText(body.content, 'content', 512 * 1024 + 8);
|
|
451
|
+
const expectedRevision = requireFileRevision(body.expectedRevision);
|
|
452
|
+
const result = await context.workbench.executeMutation(requestId, 'file.write', { fileId, expectedRevision, ...contextGuard }, () => context.workbench.writeFileContent(contextGuard, {
|
|
453
|
+
fileId,
|
|
454
|
+
content,
|
|
455
|
+
expectedRevision,
|
|
456
|
+
}));
|
|
457
|
+
sendJson(response, 200, result);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (method === 'POST' && (path === '/git/stage' || path === '/git/unstage')) {
|
|
461
|
+
assertMutation(request, context.nonce, context.origin, 'git_mutation_forbidden');
|
|
462
|
+
assertGitUserGesture(request);
|
|
463
|
+
const body = requireRecord(await readJson(request), 'Git stage request');
|
|
464
|
+
assertOnlyKeys(body, [
|
|
465
|
+
'requestId',
|
|
466
|
+
'expectedContextRevision',
|
|
467
|
+
'workspaceId',
|
|
468
|
+
'fileIds',
|
|
469
|
+
'expectedRepositoryRevision',
|
|
470
|
+
]);
|
|
471
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
472
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
473
|
+
const fileIds = requireStringArray(body.fileIds, 'fileIds', 200, 256);
|
|
474
|
+
const expectedRepositoryRevision = requireGitRevision(body.expectedRepositoryRevision);
|
|
475
|
+
const action = path === '/git/stage' ? 'stage' : 'unstage';
|
|
476
|
+
const result = await context.workbench.executeMutation(requestId, 'git.mutation', { action, fileIds, expectedRepositoryRevision, ...contextGuard }, async () => action === 'stage'
|
|
477
|
+
? context.workbench.gitStage(contextGuard, { fileIds, expectedRepositoryRevision })
|
|
478
|
+
: context.workbench.gitUnstage(contextGuard, { fileIds, expectedRepositoryRevision }));
|
|
479
|
+
sendJson(response, 200, result);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (method === 'POST' && path === '/git/commit') {
|
|
483
|
+
assertMutation(request, context.nonce, context.origin, 'git_mutation_forbidden');
|
|
484
|
+
assertGitUserGesture(request);
|
|
485
|
+
const body = requireRecord(await readJson(request), 'Git commit request');
|
|
486
|
+
assertOnlyKeys(body, [
|
|
487
|
+
'requestId',
|
|
488
|
+
'expectedContextRevision',
|
|
489
|
+
'workspaceId',
|
|
490
|
+
'message',
|
|
491
|
+
'expectedRepositoryRevision',
|
|
492
|
+
]);
|
|
493
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
494
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
495
|
+
const message = requireText(body.message, 'message', 2000);
|
|
496
|
+
const expectedRepositoryRevision = requireGitRevision(body.expectedRepositoryRevision);
|
|
497
|
+
const result = await context.workbench.executeMutation(requestId, 'git.commit', { message, expectedRepositoryRevision, ...contextGuard }, async () => context.workbench.gitCommit(contextGuard, { message, expectedRepositoryRevision }));
|
|
498
|
+
sendJson(response, 200, result);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
425
501
|
if (method === 'GET' && path === '/git/log') {
|
|
426
502
|
const contextGuard = requireContextGuardQuery(url);
|
|
427
503
|
sendJson(response, 200, await context.workbench.gitLog(contextGuard, {
|
|
@@ -569,6 +645,41 @@ function assertMutation(request, nonce, origin, forbiddenCode = 'request_forbidd
|
|
|
569
645
|
throw new HttpProblem(415, 'Mutations require application/json.');
|
|
570
646
|
}
|
|
571
647
|
}
|
|
648
|
+
function requireStringArray(value, name, maxLength, maxItemBytes) {
|
|
649
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > maxLength) {
|
|
650
|
+
throw new HttpProblem(400, `${name} must list 1 through ${maxLength} items.`);
|
|
651
|
+
}
|
|
652
|
+
return value.map(item => {
|
|
653
|
+
if (typeof item !== 'string' || item.length === 0 || item.length > maxItemBytes) {
|
|
654
|
+
throw new HttpProblem(400, `${name} contains an invalid item.`);
|
|
655
|
+
}
|
|
656
|
+
return item;
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function requireGitRevision(value) {
|
|
660
|
+
const text = requireText(value, 'expectedRepositoryRevision', 64);
|
|
661
|
+
if (!/^[0-9a-f]{40,64}$/u.test(text)) {
|
|
662
|
+
throw new HttpProblem(400, 'expectedRepositoryRevision must be a git revision.');
|
|
663
|
+
}
|
|
664
|
+
return text;
|
|
665
|
+
}
|
|
666
|
+
function assertFileUserGesture(request) {
|
|
667
|
+
if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'file-mutation-v1') {
|
|
668
|
+
throw new HttpProblem(403, 'File mutations require an explicit browser user gesture.', 'file_user_gesture_required');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function requireFileRevision(value) {
|
|
672
|
+
const text = requireText(value, 'expectedRevision', 64);
|
|
673
|
+
if (!/^[0-9a-f]{64}$/u.test(text)) {
|
|
674
|
+
throw new HttpProblem(400, 'expectedRevision must be a file content revision.');
|
|
675
|
+
}
|
|
676
|
+
return text;
|
|
677
|
+
}
|
|
678
|
+
function assertGitUserGesture(request) {
|
|
679
|
+
if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'git-mutation-v1') {
|
|
680
|
+
throw new HttpProblem(403, 'Git mutations require an explicit browser user gesture.', 'git_user_gesture_required');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
572
683
|
function assertTerminalUserGesture(request) {
|
|
573
684
|
if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'terminal-create-v1') {
|
|
574
685
|
throw new HttpProblem(403, 'Terminal creation requires an explicit browser user gesture.', 'terminal_user_gesture_required');
|
|
@@ -43,6 +43,7 @@ export declare class WebWorkbenchController {
|
|
|
43
43
|
private readonly workspaceMutationOwners;
|
|
44
44
|
private readonly pendingWorkspaceMutationStates;
|
|
45
45
|
private fileService;
|
|
46
|
+
private fileWriteService;
|
|
46
47
|
private gitService;
|
|
47
48
|
private reviewService;
|
|
48
49
|
private contextRevisionValue;
|
|
@@ -100,7 +101,53 @@ export declare class WebWorkbenchController {
|
|
|
100
101
|
readToolDetail(callId: string, offsetBytes: number, limitBytes: number, context?: WebContextGuardV1): Promise<WebToolDetailPageV1>;
|
|
101
102
|
listFiles(context: WebContextGuardV1, input: Parameters<FileReadServiceV1['list']>[0]): import("./file-read-service").WebFileTreePageV1;
|
|
102
103
|
readFileContent(context: WebContextGuardV1, input: Parameters<FileReadServiceV1['readContent']>[0]): import("./file-read-service").WebFileContentPageV1;
|
|
104
|
+
/** v0.3.14 — guarded CAS write for an existing in-root text file. */
|
|
105
|
+
writeFileContent(context: WebContextGuardV1, input: {
|
|
106
|
+
readonly fileId: string;
|
|
107
|
+
readonly content: string;
|
|
108
|
+
readonly expectedRevision: string;
|
|
109
|
+
}): {
|
|
110
|
+
readonly fileId: string;
|
|
111
|
+
readonly revision: string;
|
|
112
|
+
readonly sizeBytes: number;
|
|
113
|
+
};
|
|
103
114
|
gitStatus(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['status']>[0]): Promise<import("./git-read-model-service").WebGitStatusV1>;
|
|
115
|
+
searchFiles(context: WebContextGuardV1, input: {
|
|
116
|
+
readonly query: string;
|
|
117
|
+
readonly scope: 'name' | 'content';
|
|
118
|
+
readonly limit?: number;
|
|
119
|
+
}): Readonly<{
|
|
120
|
+
revision: string;
|
|
121
|
+
items: readonly Readonly<{
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
path: string;
|
|
125
|
+
}>[];
|
|
126
|
+
truncated: boolean;
|
|
127
|
+
}>;
|
|
128
|
+
/** v0.3.12 S3 — guarded stage/unstage/commit with repository revision CAS. */
|
|
129
|
+
gitStage(context: WebContextGuardV1, input: {
|
|
130
|
+
readonly fileIds: readonly string[];
|
|
131
|
+
readonly expectedRepositoryRevision: string;
|
|
132
|
+
}): Promise<{
|
|
133
|
+
readonly repositoryRevision: string;
|
|
134
|
+
}>;
|
|
135
|
+
gitUnstage(context: WebContextGuardV1, input: {
|
|
136
|
+
readonly fileIds: readonly string[];
|
|
137
|
+
readonly expectedRepositoryRevision: string;
|
|
138
|
+
}): Promise<{
|
|
139
|
+
readonly repositoryRevision: string;
|
|
140
|
+
}>;
|
|
141
|
+
gitCommit(context: WebContextGuardV1, input: {
|
|
142
|
+
readonly message: string;
|
|
143
|
+
readonly expectedRepositoryRevision: string;
|
|
144
|
+
}): Promise<{
|
|
145
|
+
readonly repositoryRevision: string;
|
|
146
|
+
readonly commitSha: string;
|
|
147
|
+
}>;
|
|
148
|
+
private resolveSafeGitPaths;
|
|
149
|
+
private assertGitRepositoryRevision;
|
|
150
|
+
private fileContextRevision;
|
|
104
151
|
gitLog(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['log']>[0]): Promise<import("./git-read-model-service").WebGitLogPageV1>;
|
|
105
152
|
gitDiff(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<import("./git-read-model-service").WebGitDiffPageV1>;
|
|
106
153
|
review(context: WebContextGuardV1): Promise<import("./review-service").WebReviewSnapshotV1>;
|