orizu 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/artifact-archive.js +152 -0
- package/dist/connectors-cli.js +80 -0
- package/dist/credentials.js +4 -1
- package/dist/daytona-slice-rehearsal.js +255 -0
- package/dist/daytona-slice.js +441 -0
- package/dist/egress-allowlist-cli.js +132 -0
- package/dist/egress-policy.js +159 -0
- package/dist/git-credential.js +131 -0
- package/dist/github-setup.js +226 -0
- package/dist/help.js +276 -21
- package/dist/hosted-bootstrap-rehearsal.js +235 -0
- package/dist/hosted-bootstrap.js +365 -0
- package/dist/hosted-broker-server.js +220 -0
- package/dist/hosted-git-http-server.js +136 -0
- package/dist/hosted-harness-claude.js +493 -0
- package/dist/hosted-harness-opencode.js +729 -0
- package/dist/hosted-harness.js +60 -0
- package/dist/hosted-loop.js +477 -0
- package/dist/hosted-run-event-sink.js +499 -0
- package/dist/hosted-runtime-assets.js +216 -0
- package/dist/hosted-session-cli.js +642 -0
- package/dist/index.js +945 -258
- package/dist/kill-switch-cli.js +89 -0
- package/dist/manifests-cli.js +143 -0
- package/dist/preview-runtime.js +4 -1
- package/dist/sandbox-provider.js +238 -0
- package/dist/secret-redaction.js +112 -0
- package/dist/session-timeline.js +103 -0
- package/dist/setup-prompt.js +4 -2
- package/dist/skill-installer.js +9 -1
- package/dist/smoke.js +250 -0
- package/dist/task-assignment-manifest.js +96 -0
- package/dist/vercel-sandbox-provider.js +260 -0
- package/dist/workbench-cli.js +493 -0
- package/dist/workspace-sync.js +924 -0
- package/dist/workspace.js +868 -57
- package/package.json +4 -1
- package/src/artifact-archive.ts +182 -0
- package/src/connectors-cli.ts +115 -0
- package/src/credentials.ts +6 -2
- package/src/daytona-slice-rehearsal.ts +320 -0
- package/src/daytona-slice.ts +598 -0
- package/src/egress-allowlist-cli.ts +147 -0
- package/src/egress-policy.ts +188 -0
- package/src/git-credential.ts +155 -0
- package/src/github-setup.ts +366 -0
- package/src/help.ts +277 -21
- package/src/hosted-bootstrap-rehearsal.ts +312 -0
- package/src/hosted-bootstrap.ts +571 -0
- package/src/hosted-broker-server.ts +278 -0
- package/src/hosted-git-http-server.ts +164 -0
- package/src/hosted-harness-claude.ts +621 -0
- package/src/hosted-harness-opencode.ts +823 -0
- package/src/hosted-harness.ts +124 -0
- package/src/hosted-loop.ts +680 -0
- package/src/hosted-run-event-sink.ts +632 -0
- package/src/hosted-runtime-assets.ts +272 -0
- package/src/hosted-session-cli.ts +844 -0
- package/src/index.ts +1102 -317
- package/src/kill-switch-cli.ts +113 -0
- package/src/manifests-cli.ts +168 -0
- package/src/preview-runtime.ts +14 -9
- package/src/sandbox-provider.ts +440 -0
- package/src/secret-redaction.ts +124 -0
- package/src/session-timeline.ts +152 -0
- package/src/setup-prompt.ts +4 -2
- package/src/skill-installer.ts +13 -1
- package/src/smoke.ts +342 -0
- package/src/task-assignment-manifest.ts +164 -0
- package/src/vercel-sandbox-provider.ts +388 -0
- package/src/workbench-cli.ts +701 -0
- package/src/workspace-sync.ts +1148 -0
- package/src/workspace.ts +1051 -64
- package/vendor/skills/orizu-cli/SKILL.md +8 -4
- package/vendor/skills/orizu-cli/references/cli-reference.md +12 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-team agent kill switch CLI (ALI-1007 / G6):
|
|
3
|
+
* orizu team kill-agents <team> [--reason <text>]
|
|
4
|
+
* orizu team release-agents <team>
|
|
5
|
+
*
|
|
6
|
+
* `kill-agents` ENGAGES the switch — ends every active session of the team and
|
|
7
|
+
* revokes its agent tokens "in seconds"; `release-agents` re-enables minting
|
|
8
|
+
* (killed sessions stay ended). Human ADMIN only: the server rejects agent
|
|
9
|
+
* bearers and non-admins. Pure command logic with an injected fetcher so the
|
|
10
|
+
* entry point owns argument parsing and human/JSON output.
|
|
11
|
+
*/
|
|
12
|
+
import { authedFetch } from './http.js';
|
|
13
|
+
async function responseMessage(response) {
|
|
14
|
+
try {
|
|
15
|
+
const data = (await response.clone().json());
|
|
16
|
+
const error = typeof data.error === 'string' ? data.error : null;
|
|
17
|
+
return error || response.statusText || String(response.status);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return response.statusText || String(response.status);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function fetcherFrom(io) {
|
|
24
|
+
return io.fetcher ?? authedFetch;
|
|
25
|
+
}
|
|
26
|
+
function argValue(args, flag) {
|
|
27
|
+
const index = args.indexOf(flag);
|
|
28
|
+
if (index === -1 || index + 1 >= args.length || args[index + 1].startsWith('--')) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return args[index + 1];
|
|
32
|
+
}
|
|
33
|
+
function firstPositional(args) {
|
|
34
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
35
|
+
const arg = args[i];
|
|
36
|
+
if (arg.startsWith('--')) {
|
|
37
|
+
if (argValue(args, arg) !== null) {
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
return arg;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function emit(io, payload, human) {
|
|
47
|
+
io.print(io.json ? JSON.stringify(payload) : human);
|
|
48
|
+
}
|
|
49
|
+
function printErr(io, message) {
|
|
50
|
+
;
|
|
51
|
+
(io.printErr ?? io.print)(message);
|
|
52
|
+
}
|
|
53
|
+
export async function killSwitchCommand(args, io) {
|
|
54
|
+
const subcommand = args[0];
|
|
55
|
+
if (subcommand !== 'kill-agents' && subcommand !== 'release-agents') {
|
|
56
|
+
printErr(io, 'Usage: orizu team <kill-agents|release-agents> <team> [--reason <text>] [--json]');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
const teamSlug = firstPositional(args.slice(1));
|
|
60
|
+
if (!teamSlug) {
|
|
61
|
+
printErr(io, `Usage: orizu team ${subcommand} <team>${subcommand === 'kill-agents' ? ' [--reason <text>]' : ''} [--json]`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const path = `/api/cli/teams/${encodeURIComponent(teamSlug)}/agent-kill-switch`;
|
|
65
|
+
if (subcommand === 'kill-agents') {
|
|
66
|
+
const reason = argValue(args, '--reason');
|
|
67
|
+
const response = await fetcherFrom(io)(path, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(reason ? { reason } : {}),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
printErr(io, `kill-agents failed (${response.status}): ${await responseMessage(response)}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const data = (await response.json());
|
|
77
|
+
emit(io, data, `agent kill switch ENGAGED for ${teamSlug} ` +
|
|
78
|
+
`(${data.sessionsEnded ?? 0} sessions ended, ${data.tokensRevoked ?? 0} tokens revoked)`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const response = await fetcherFrom(io)(path, { method: 'DELETE' });
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
printErr(io, `release-agents failed (${response.status}): ${await responseMessage(response)}`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
const data = (await response.json());
|
|
87
|
+
emit(io, data, `agent kill switch RELEASED for ${teamSlug} (killed sessions stay ended)`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promotion manifests CLI (ALI-907): list | show | approve | reject | apply.
|
|
3
|
+
* Pure command logic with an injected fetcher and project resolver; the entry
|
|
4
|
+
* point owns argument parsing and human/JSON output. apply is idempotent on the
|
|
5
|
+
* server — re-applying returns the stored outcome.
|
|
6
|
+
*/
|
|
7
|
+
import { authedFetch } from './http.js';
|
|
8
|
+
const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply']);
|
|
9
|
+
async function responseMessage(response) {
|
|
10
|
+
try {
|
|
11
|
+
const data = (await response.clone().json());
|
|
12
|
+
const error = typeof data.error === 'string' ? data.error : null;
|
|
13
|
+
const message = typeof data.message === 'string' ? data.message : null;
|
|
14
|
+
return error || message || response.statusText || String(response.status);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return response.statusText || String(response.status);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function requireOk(response, action) {
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error(`${action} failed (${response.status}): ${await responseMessage(response)}`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return (await response.json());
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function fetcherFrom(io) {
|
|
32
|
+
return io.fetcher ?? authedFetch;
|
|
33
|
+
}
|
|
34
|
+
function argValue(args, flag) {
|
|
35
|
+
const index = args.indexOf(flag);
|
|
36
|
+
if (index === -1 || index + 1 >= args.length || args[index + 1].startsWith('--')) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return args[index + 1];
|
|
40
|
+
}
|
|
41
|
+
function positionalArgs(args) {
|
|
42
|
+
const values = [];
|
|
43
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (arg.startsWith('--')) {
|
|
46
|
+
if (argValue(args, arg) !== null) {
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
values.push(arg);
|
|
52
|
+
}
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
async function requireProjectSlug(io, args) {
|
|
56
|
+
if (!io.resolveProjectSlug) {
|
|
57
|
+
throw new Error('Project resolver unavailable');
|
|
58
|
+
}
|
|
59
|
+
return io.resolveProjectSlug(argValue(args, '--project'));
|
|
60
|
+
}
|
|
61
|
+
function emit(io, payload, human) {
|
|
62
|
+
io.print(io.json ? JSON.stringify(payload) : human);
|
|
63
|
+
}
|
|
64
|
+
function manifestLine(manifest) {
|
|
65
|
+
const id = typeof manifest.id === 'string' ? manifest.id : '(unknown)';
|
|
66
|
+
const status = typeof manifest.status === 'string' ? manifest.status : '(unknown)';
|
|
67
|
+
const actionType = typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)';
|
|
68
|
+
return `${id} ${status} ${actionType}`;
|
|
69
|
+
}
|
|
70
|
+
function asRecord(value) {
|
|
71
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
72
|
+
}
|
|
73
|
+
function num(value) {
|
|
74
|
+
return typeof value === 'number' ? value : 0;
|
|
75
|
+
}
|
|
76
|
+
// repo_merge manifests (ALI-972) render the compare summary from stored evidence
|
|
77
|
+
// — no live GitHub call — so a reviewer can approve informed without a PR.
|
|
78
|
+
function repoMergeShow(manifest) {
|
|
79
|
+
const proposed = asRecord(manifest.proposedState);
|
|
80
|
+
const compare = asRecord(asRecord(manifest.evidence).compare);
|
|
81
|
+
const branch = typeof proposed.branch === 'string' ? proposed.branch : '(branch)';
|
|
82
|
+
const defaultBranch = typeof proposed.defaultBranch === 'string' ? proposed.defaultBranch : '(default)';
|
|
83
|
+
const files = num(compare.filesChanged || proposed.filesChanged);
|
|
84
|
+
const additions = num(compare.additions ?? proposed.additions);
|
|
85
|
+
const deletions = num(compare.deletions ?? proposed.deletions);
|
|
86
|
+
const lines = [
|
|
87
|
+
manifestLine(manifest),
|
|
88
|
+
` ${branch} -> ${defaultBranch}`,
|
|
89
|
+
` ${files} files, +${additions}/-${deletions}`,
|
|
90
|
+
];
|
|
91
|
+
const compareFiles = Array.isArray(compare.files) ? compare.files : [];
|
|
92
|
+
for (const file of compareFiles.slice(0, 100)) {
|
|
93
|
+
const status = typeof file.status === 'string' ? file.status : 'modified';
|
|
94
|
+
const filename = typeof file.filename === 'string' ? file.filename : '(file)';
|
|
95
|
+
lines.push(` ${status} ${filename} (+${num(file.additions)}/-${num(file.deletions)})`);
|
|
96
|
+
}
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
export async function manifestsCommand(args, io) {
|
|
100
|
+
const positional = positionalArgs(args);
|
|
101
|
+
const subcommand = positional[0];
|
|
102
|
+
if (subcommand === 'list') {
|
|
103
|
+
const projectSlug = await requireProjectSlug(io, args);
|
|
104
|
+
const status = argValue(args, '--status');
|
|
105
|
+
const query = status
|
|
106
|
+
? `?project=${encodeURIComponent(projectSlug)}&status=${encodeURIComponent(status)}`
|
|
107
|
+
: `?project=${encodeURIComponent(projectSlug)}`;
|
|
108
|
+
const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`);
|
|
109
|
+
const data = await requireOk(response, 'Manifest list');
|
|
110
|
+
const manifests = Array.isArray(data.manifests) ? data.manifests : [];
|
|
111
|
+
emit(io, { manifests }, ['promotion manifests', ...manifests.map(manifestLine)].join('\n'));
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
if (subcommand === 'show') {
|
|
115
|
+
const id = positional[1];
|
|
116
|
+
if (!id) {
|
|
117
|
+
throw new Error('Usage: orizu manifests show <id> [--json]');
|
|
118
|
+
}
|
|
119
|
+
const response = await fetcherFrom(io)(`/api/cli/promotion-manifests/${encodeURIComponent(id)}`);
|
|
120
|
+
const data = await requireOk(response, 'Manifest show');
|
|
121
|
+
const manifest = (data.manifest ?? {});
|
|
122
|
+
const human = manifest.actionType === 'repo_merge' ? repoMergeShow(manifest) : manifestLine(manifest);
|
|
123
|
+
emit(io, { manifest }, human);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
if (subcommand && MANIFEST_ACTIONS.has(subcommand)) {
|
|
127
|
+
const id = positional[1];
|
|
128
|
+
if (!id) {
|
|
129
|
+
throw new Error(`Usage: orizu manifests ${subcommand} <id> [--json]`);
|
|
130
|
+
}
|
|
131
|
+
const response = await fetcherFrom(io)(`/api/cli/promotion-manifests/${encodeURIComponent(id)}`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ action: subcommand }),
|
|
135
|
+
});
|
|
136
|
+
const data = await requireOk(response, `Manifest ${subcommand}`);
|
|
137
|
+
const manifest = (data.manifest ?? {});
|
|
138
|
+
emit(io, { manifest }, manifestLine(manifest));
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
io.print('Usage: orizu manifests <list|show|approve|reject|apply> ... [--json]');
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
package/dist/preview-runtime.js
CHANGED
|
@@ -50,6 +50,9 @@ export const PREVIEW_ALLOWED_IMPORTS = {
|
|
|
50
50
|
'@/components/templates/ranking/RankingList': ['RankingList'],
|
|
51
51
|
'@/components/templates/rating/SingleItemRater': ['SingleItemRater'],
|
|
52
52
|
};
|
|
53
|
+
function isNodeError(error) {
|
|
54
|
+
return error instanceof Error;
|
|
55
|
+
}
|
|
53
56
|
const SUPPORTED_SCHEMA_KEYS = new Set(['type', 'required', 'properties', 'items', 'enum']);
|
|
54
57
|
export function validatePreviewInputs(options) {
|
|
55
58
|
const issues = [];
|
|
@@ -827,7 +830,7 @@ async function listenOnAvailablePort(server) {
|
|
|
827
830
|
}
|
|
828
831
|
catch (error) {
|
|
829
832
|
server.removeAllListeners('error');
|
|
830
|
-
if (error
|
|
833
|
+
if (!isNodeError(error) || error.code !== 'EADDRINUSE')
|
|
831
834
|
throw error;
|
|
832
835
|
}
|
|
833
836
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orizu-owned sandbox provider seam (ALI-973 / WS-F, per ADR-003).
|
|
3
|
+
*
|
|
4
|
+
* ADR-003 requires every provider call (GitHub, Daytona) to sit behind an
|
|
5
|
+
* Orizu-owned interface so the durable contracts never couple to whichever SDK
|
|
6
|
+
* wins the quarter. This module defines `SandboxProvider` / `SandboxSession` and
|
|
7
|
+
* ships two implementations:
|
|
8
|
+
*
|
|
9
|
+
* - `createDaytonaProvider(config)` — a THIN adapter over the Daytona
|
|
10
|
+
* TypeScript SDK (`@daytonaio/sdk`). The SDK is imported LAZILY (dynamic
|
|
11
|
+
* import through a non-literal specifier) so the CLI works without it
|
|
12
|
+
* installed, the app build never resolves it, and tests never load it.
|
|
13
|
+
* - `createLocalSimProvider(config)` — a temp-dir + real-`git`-subprocess
|
|
14
|
+
* simulation used by the unit tests AND by the no-credentials local-sim
|
|
15
|
+
* rehearsal. It is a genuine rehearsal (real clone/commit/push against a
|
|
16
|
+
* local bare repo), not a mock.
|
|
17
|
+
*
|
|
18
|
+
* SDK-vs-docs note (verified against daytona.io/docs, 2026-07-02): the real
|
|
19
|
+
* `git.clone` signature is
|
|
20
|
+
* clone(url, path, branch?, commitId?, username?, password?, insecureSkipTls?)
|
|
21
|
+
* i.e. a `commitId` argument sits BETWEEN `branch` and `username` — the ALI-973
|
|
22
|
+
* brief documented `clone(url, path, branch, username, password)`. The adapter
|
|
23
|
+
* passes `undefined` for `commitId` so the Orizu seam keeps its clean per-call
|
|
24
|
+
* credential shape. `git.push(path, username?, password?)` matches the brief.
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from 'child_process';
|
|
27
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
28
|
+
import { tmpdir } from 'os';
|
|
29
|
+
import { basename, dirname, join } from 'path';
|
|
30
|
+
// The package name lives in a variable so TypeScript treats the dynamic import
|
|
31
|
+
// as `any` (no static module resolution / no ts2307 at app build time) and so
|
|
32
|
+
// no bundler traces it. This is what keeps the SDK genuinely optional.
|
|
33
|
+
//
|
|
34
|
+
// PIN CONTRACT: `wrapDaytonaSandbox` hard-codes the POSITIONAL git.clone
|
|
35
|
+
// signature (url, path, branch, commitId, username, password) verified against
|
|
36
|
+
// @daytonaio/sdk@0.193.0. package.json pins that version EXACTLY (no caret) and
|
|
37
|
+
// packages/cli/bun.lock records it, so a minor bump cannot silently shift the
|
|
38
|
+
// argument order out from under this adapter. Re-verify the signature before
|
|
39
|
+
// changing the pin.
|
|
40
|
+
//
|
|
41
|
+
// TRANSITIVE-DEPENDENCY NOTE: @daytonaio/sdk itself drags in a substantial
|
|
42
|
+
// tree — @aws-sdk/client-s3, the OpenTelemetry SDK/exporters, and friends —
|
|
43
|
+
// into packages/cli's dependency graph. `bun install --cwd packages/cli`
|
|
44
|
+
// resolves and downloads all of it even though this file only ever reaches
|
|
45
|
+
// the package via the lazy `import(specifier)` above. That's judged
|
|
46
|
+
// acceptable because: (1) this is a founder-only, not end-user-shipped,
|
|
47
|
+
// script — packages/cli's app build and published `dist/` never bundle or
|
|
48
|
+
// execute this path; (2) the lazy import means the SDK (and its transitive
|
|
49
|
+
// tree) is never loaded into the running process unless `runDaytonaSlice`
|
|
50
|
+
// actually selects the `daytona` provider; (3) `bun install`'s resolve-time
|
|
51
|
+
// cost is a one-time developer/CI tax on this package, not a runtime or
|
|
52
|
+
// shipped-artifact cost. Re-evaluate if @daytonaio/sdk ever becomes a
|
|
53
|
+
// dependency of a package that IS shipped to end users.
|
|
54
|
+
const DAYTONA_SDK_SPECIFIER = '@daytonaio/sdk';
|
|
55
|
+
async function loadDaytonaModule() {
|
|
56
|
+
try {
|
|
57
|
+
const specifier = DAYTONA_SDK_SPECIFIER;
|
|
58
|
+
const mod = (await import(specifier));
|
|
59
|
+
if (!mod || typeof mod.Daytona !== 'function') {
|
|
60
|
+
throw new Error('module did not export a Daytona class');
|
|
61
|
+
}
|
|
62
|
+
return mod;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(`Daytona SDK unavailable (${detail}). Install it before the live run: \`bun install --cwd packages/cli\` (declares @daytonaio/sdk).`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function wrapDaytonaSandbox(sandbox) {
|
|
70
|
+
return {
|
|
71
|
+
id: sandbox.id,
|
|
72
|
+
async gitClone(params) {
|
|
73
|
+
// commitId (4th arg) is undefined — clean per-call credential shape.
|
|
74
|
+
await sandbox.git.clone(params.url, params.path, params.branch, undefined, params.username, params.password);
|
|
75
|
+
},
|
|
76
|
+
async gitPush(params) {
|
|
77
|
+
await sandbox.git.push(params.path, params.username, params.password);
|
|
78
|
+
},
|
|
79
|
+
async exec(command, opts) {
|
|
80
|
+
const res = await sandbox.process.executeCommand(command, opts?.cwd);
|
|
81
|
+
const stdout = typeof res.result === 'string' ? res.result : res.artifacts?.stdout ?? '';
|
|
82
|
+
const stderr = res.stderr ?? res.artifacts?.stderr;
|
|
83
|
+
// A missing exitCode is treated as a FAILURE (1), never masked to success:
|
|
84
|
+
// a live clone/push that returns no code must not read as green.
|
|
85
|
+
const exitCode = typeof res.exitCode === 'number' ? res.exitCode : 1;
|
|
86
|
+
return { stdout, exitCode, stderr };
|
|
87
|
+
},
|
|
88
|
+
async readFile(path) {
|
|
89
|
+
const data = await sandbox.fs.downloadFile(path);
|
|
90
|
+
if (typeof data === 'string')
|
|
91
|
+
return data;
|
|
92
|
+
return Buffer.from(data).toString('utf8');
|
|
93
|
+
},
|
|
94
|
+
async writeFile(path, content) {
|
|
95
|
+
await sandbox.fs.uploadFile(Buffer.from(content, 'utf8'), path);
|
|
96
|
+
},
|
|
97
|
+
async fileExists(path) {
|
|
98
|
+
try {
|
|
99
|
+
await sandbox.fs.getFileDetails(path);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
async destroy() {
|
|
107
|
+
await sandbox.delete();
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function createDaytonaProvider(config, deps = {}) {
|
|
112
|
+
const load = deps.loadModule ?? loadDaytonaModule;
|
|
113
|
+
let clientPromise = null;
|
|
114
|
+
const client = () => {
|
|
115
|
+
if (!clientPromise) {
|
|
116
|
+
clientPromise = (async () => {
|
|
117
|
+
const mod = await load();
|
|
118
|
+
return new mod.Daytona({
|
|
119
|
+
apiKey: config.apiKey,
|
|
120
|
+
apiUrl: config.apiUrl,
|
|
121
|
+
target: config.target,
|
|
122
|
+
});
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
125
|
+
return clientPromise;
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
kind: 'daytona',
|
|
129
|
+
async createSandbox(opts) {
|
|
130
|
+
const daytona = await client();
|
|
131
|
+
const params = {};
|
|
132
|
+
if (opts?.language)
|
|
133
|
+
params.language = opts.language;
|
|
134
|
+
if (opts?.snapshot)
|
|
135
|
+
params.snapshot = opts.snapshot;
|
|
136
|
+
if (opts?.envVars)
|
|
137
|
+
params.envVars = opts.envVars;
|
|
138
|
+
if (opts?.labels)
|
|
139
|
+
params.labels = opts.labels;
|
|
140
|
+
// ORPHAN-SANDBOX WINDOW: `daytona.create(...)` is a single SDK call that
|
|
141
|
+
// provisions the remote sandbox AND resolves with its handle as one unit.
|
|
142
|
+
// If it throws AFTER the remote sandbox exists but BEFORE this awaits
|
|
143
|
+
// returns (e.g. the create succeeded server-side but the response never
|
|
144
|
+
// reached us), the caller (runDaytonaSlice) never receives a
|
|
145
|
+
// SandboxSession and has no id to destroy — the sandbox is orphaned on
|
|
146
|
+
// Daytona's side with nothing in our process able to name it. This is
|
|
147
|
+
// inherent to the SDK's create-returns-handle shape, not fixable by
|
|
148
|
+
// catching here (there is no id to catch). `--keep`/manual cleanup in
|
|
149
|
+
// the Daytona dashboard is the backstop. A create-then-fetch-id two-phase
|
|
150
|
+
// API (create returns an id immediately, a separate call waits for
|
|
151
|
+
// readiness) would close this window if Daytona ever ships one.
|
|
152
|
+
const sandbox = await daytona.create(Object.keys(params).length ? params : undefined);
|
|
153
|
+
return wrapDaytonaSandbox(sandbox);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function runIn(bin, args, cwd, home) {
|
|
158
|
+
// Strip git's repo-context env (set by git when this code runs inside a
|
|
159
|
+
// hook, e.g. pre-push running `bun test`) so sim git commands operate on
|
|
160
|
+
// the sandbox cwd — not whatever repo GIT_DIR points at. See
|
|
161
|
+
// daytona-slice-rehearsal.ts for the core.bare=true corruption this caused.
|
|
162
|
+
const env = {
|
|
163
|
+
...process.env,
|
|
164
|
+
HOME: home,
|
|
165
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
166
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
167
|
+
};
|
|
168
|
+
delete env.GIT_DIR;
|
|
169
|
+
delete env.GIT_WORK_TREE;
|
|
170
|
+
delete env.GIT_INDEX_FILE;
|
|
171
|
+
delete env.GIT_OBJECT_DIRECTORY;
|
|
172
|
+
delete env.GIT_COMMON_DIR;
|
|
173
|
+
const res = spawnSync(bin, args, { cwd, encoding: 'utf8', env });
|
|
174
|
+
return { stdout: res.stdout ?? '', stderr: res.stderr ?? '', exitCode: res.status ?? 1 };
|
|
175
|
+
}
|
|
176
|
+
function makeLocalSimSession(id, root, keepOnDestroy) {
|
|
177
|
+
// A self-contained git identity so commits work regardless of host config.
|
|
178
|
+
writeFileSync(join(root, '.gitconfig'), '[user]\n\temail = orizu-slice@local.sim\n\tname = Orizu Slice\n' +
|
|
179
|
+
'[init]\n\tdefaultBranch = main\n[safe]\n\tdirectory = *\n');
|
|
180
|
+
const inRoot = (p) => join(root, p);
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
async gitClone(params) {
|
|
184
|
+
const args = ['clone'];
|
|
185
|
+
if (params.branch)
|
|
186
|
+
args.push('--branch', params.branch);
|
|
187
|
+
// Clean per-call shape: the clone URL is the bare repo path; the
|
|
188
|
+
// per-call username/password are honored by the transport but never
|
|
189
|
+
// persisted into the cloned repo's config (mirrors the contract we
|
|
190
|
+
// require of Daytona). Nothing to sanitize because nothing is embedded.
|
|
191
|
+
args.push(params.url, params.path);
|
|
192
|
+
const r = runIn('git', args, root, root);
|
|
193
|
+
if (r.exitCode !== 0) {
|
|
194
|
+
throw new Error(`git clone failed: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
async gitPush(params) {
|
|
198
|
+
const r = runIn('git', ['-C', params.path, 'push'], root, root);
|
|
199
|
+
if (r.exitCode !== 0) {
|
|
200
|
+
throw new Error(`git push failed: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
async exec(command, opts) {
|
|
204
|
+
const cwd = opts?.cwd ? inRoot(opts.cwd) : root;
|
|
205
|
+
const r = runIn('bash', ['-c', command], cwd, root);
|
|
206
|
+
return { stdout: r.stdout, exitCode: r.exitCode, stderr: r.stderr };
|
|
207
|
+
},
|
|
208
|
+
async readFile(path) {
|
|
209
|
+
return readFileSync(inRoot(path), 'utf8');
|
|
210
|
+
},
|
|
211
|
+
async writeFile(path, content) {
|
|
212
|
+
const abs = inRoot(path);
|
|
213
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
214
|
+
writeFileSync(abs, content);
|
|
215
|
+
},
|
|
216
|
+
async fileExists(path) {
|
|
217
|
+
return existsSync(inRoot(path));
|
|
218
|
+
},
|
|
219
|
+
async destroy() {
|
|
220
|
+
if (!keepOnDestroy) {
|
|
221
|
+
rmSync(root, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function createLocalSimProvider(config = {}) {
|
|
227
|
+
let counter = 0;
|
|
228
|
+
return {
|
|
229
|
+
kind: 'local-sim',
|
|
230
|
+
async createSandbox() {
|
|
231
|
+
counter += 1;
|
|
232
|
+
const parent = config.rootDir ?? tmpdir();
|
|
233
|
+
mkdirSync(parent, { recursive: true });
|
|
234
|
+
const root = mkdtempSync(join(parent, 'orizu-sim-'));
|
|
235
|
+
return makeLocalSimSession(`local-sim-${counter}-${basename(root)}`, root, Boolean(config.keepOnDestroy));
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction for hosted-sandbox bootstrap (G3 / ALI-1004).
|
|
3
|
+
*
|
|
4
|
+
* The bootstrap orchestrator records lifecycle events to the RunAPI and may
|
|
5
|
+
* capture hook / install output. None of that may ever carry the Orizu bearer,
|
|
6
|
+
* a minted repo token, or any token-shaped value. `redactSecrets` scrubs a
|
|
7
|
+
* value two ways:
|
|
8
|
+
*
|
|
9
|
+
* 1. exact-match on caller-supplied secrets (the bearer, any token the
|
|
10
|
+
* orchestrator itself minted) — these are known verbatim, so replace them
|
|
11
|
+
* wherever they appear, even mid-string;
|
|
12
|
+
* 2. shape-match on the well-known credential prefixes GitHub, Orizu, and the
|
|
13
|
+
* model providers emit (ghs_/ghp_/gho_/ghu_/ghr_/github_pat_ installation+PAT
|
|
14
|
+
* tokens, Orizu's own `orizu_pat_`/`orizu_agent_` tokens, Anthropic
|
|
15
|
+
* `sk-ant-…` and OpenAI `sk-proj-…`/legacy `sk-…` model keys, and a generic
|
|
16
|
+
* `Bearer <blob>` header) — a defense-in-depth net for tokens the
|
|
17
|
+
* orchestrator never held a copy of (e.g. a model key echoed by a customer
|
|
18
|
+
* setup hook or an OpenCode error message).
|
|
19
|
+
*
|
|
20
|
+
* SHAPES ARE DEFENSE IN DEPTH, NOT THE GUARANTEE. The guaranteed scrub is the
|
|
21
|
+
* exact-match pass (1): any secret that will be present in the sandbox MUST be
|
|
22
|
+
* handed to the caller's secret list (the RunEventSink's `redactSecretsList`)
|
|
23
|
+
* so it is stripped verbatim wherever it appears, even mid-string and even when
|
|
24
|
+
* its shape is unknown. The shape patterns below only catch credentials no one
|
|
25
|
+
* declared; do not rely on them as the primary control.
|
|
26
|
+
*
|
|
27
|
+
* The function is pure and recursive over JSON-ish structures so an entire
|
|
28
|
+
* event payload can be passed through before it is sent to the RunAPI.
|
|
29
|
+
*/
|
|
30
|
+
export const REDACTION_PLACEHOLDER = '[redacted]';
|
|
31
|
+
// Token SHAPES, not values. Ordered longest-prefix-first so `github_pat_` wins
|
|
32
|
+
// over a hypothetical shorter alias. Each matches the prefix plus its base62/_-
|
|
33
|
+
// body. `Bearer <token>` is caught separately so an Authorization header value
|
|
34
|
+
// is scrubbed even when the token body is short.
|
|
35
|
+
const TOKEN_SHAPE_PATTERNS = [
|
|
36
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
37
|
+
/\bgh[posur]_[A-Za-z0-9_]{20,}/g,
|
|
38
|
+
// Orizu-issued credentials: `orizu_pat_` (personal access tokens, see
|
|
39
|
+
// lib/personal-access-tokens.ts — prefix + base64url body) and the future
|
|
40
|
+
// `orizu_agent_` session-token prefix. base64url alphabet is [A-Za-z0-9_-].
|
|
41
|
+
/\borizu_(agent|pat)_[A-Za-z0-9_-]{10,}/g,
|
|
42
|
+
// Model-provider API keys the agent runs with. Specific prefixes first so the
|
|
43
|
+
// bare `sk-` fallback below cannot partially shadow them:
|
|
44
|
+
// Anthropic `sk-ant-…` (real keys look like `sk-ant-api03-<blob>`);
|
|
45
|
+
// OpenAI `sk-proj-…` (project-scoped keys) and legacy bare `sk-<blob>`.
|
|
46
|
+
// The bare fallback demands 20+ base62 chars after `sk-` so it cannot fire on
|
|
47
|
+
// the hyphenated `sk-ant-`/`sk-proj-` prefixes (both break the run at the
|
|
48
|
+
// first `-`) or on short `sk-` substrings — tuned to avoid a false-positive
|
|
49
|
+
// massacre while still netting an undeclared legacy OpenAI key.
|
|
50
|
+
/\bsk-ant-[A-Za-z0-9_-]{10,}/g,
|
|
51
|
+
/\bsk-proj-[A-Za-z0-9_-]{10,}/g,
|
|
52
|
+
/\bsk-[A-Za-z0-9]{20,}\b/g,
|
|
53
|
+
/\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/g,
|
|
54
|
+
];
|
|
55
|
+
function escapeRegExp(value) {
|
|
56
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Redact a scalar string: exact secrets first, then token shapes.
|
|
60
|
+
*/
|
|
61
|
+
function redactString(input, secrets) {
|
|
62
|
+
let out = input;
|
|
63
|
+
for (const secret of secrets) {
|
|
64
|
+
if (secret.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
out = out.replace(new RegExp(escapeRegExp(secret), 'g'), REDACTION_PLACEHOLDER);
|
|
67
|
+
}
|
|
68
|
+
for (const pattern of TOKEN_SHAPE_PATTERNS) {
|
|
69
|
+
out = out.replace(pattern, match => match.toLowerCase().startsWith('bearer') ? `Bearer ${REDACTION_PLACEHOLDER}` : REDACTION_PLACEHOLDER);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Deep-redact a JSON-ish value. Strings are scrubbed; arrays/objects are walked;
|
|
75
|
+
* everything else is returned as-is. Object KEYS are never treated as secrets
|
|
76
|
+
* (a key literally named after a token is not the token), but their VALUES are.
|
|
77
|
+
*/
|
|
78
|
+
export function redactSecrets(value, options = {}) {
|
|
79
|
+
const secrets = (options.secrets ?? []).filter(s => typeof s === 'string' && s.length > 0);
|
|
80
|
+
return redactValue(value, secrets);
|
|
81
|
+
}
|
|
82
|
+
function redactValue(value, secrets) {
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
return redactString(value, secrets);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value.map(item => redactValue(item, secrets));
|
|
88
|
+
}
|
|
89
|
+
if (value && typeof value === 'object') {
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
92
|
+
out[key] = redactValue(inner, secrets);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* True when a string still contains any of the supplied secrets or a
|
|
100
|
+
* token-shaped substring — the assertion tests use it to prove an event payload
|
|
101
|
+
* left the orchestrator clean.
|
|
102
|
+
*/
|
|
103
|
+
export function containsSecret(input, secrets) {
|
|
104
|
+
for (const secret of secrets) {
|
|
105
|
+
if (secret.length > 0 && input.includes(secret))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return TOKEN_SHAPE_PATTERNS.some(pattern => {
|
|
109
|
+
pattern.lastIndex = 0;
|
|
110
|
+
return pattern.test(input);
|
|
111
|
+
});
|
|
112
|
+
}
|