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.
Files changed (76) hide show
  1. package/README.md +22 -13
  2. package/dist/artifact-archive.js +152 -0
  3. package/dist/connectors-cli.js +80 -0
  4. package/dist/credentials.js +4 -1
  5. package/dist/daytona-slice-rehearsal.js +255 -0
  6. package/dist/daytona-slice.js +441 -0
  7. package/dist/egress-allowlist-cli.js +132 -0
  8. package/dist/egress-policy.js +159 -0
  9. package/dist/git-credential.js +131 -0
  10. package/dist/github-setup.js +226 -0
  11. package/dist/help.js +276 -21
  12. package/dist/hosted-bootstrap-rehearsal.js +235 -0
  13. package/dist/hosted-bootstrap.js +365 -0
  14. package/dist/hosted-broker-server.js +220 -0
  15. package/dist/hosted-git-http-server.js +136 -0
  16. package/dist/hosted-harness-claude.js +493 -0
  17. package/dist/hosted-harness-opencode.js +729 -0
  18. package/dist/hosted-harness.js +60 -0
  19. package/dist/hosted-loop.js +477 -0
  20. package/dist/hosted-run-event-sink.js +499 -0
  21. package/dist/hosted-runtime-assets.js +216 -0
  22. package/dist/hosted-session-cli.js +642 -0
  23. package/dist/index.js +945 -258
  24. package/dist/kill-switch-cli.js +89 -0
  25. package/dist/manifests-cli.js +143 -0
  26. package/dist/preview-runtime.js +4 -1
  27. package/dist/sandbox-provider.js +238 -0
  28. package/dist/secret-redaction.js +112 -0
  29. package/dist/session-timeline.js +103 -0
  30. package/dist/setup-prompt.js +4 -2
  31. package/dist/skill-installer.js +9 -1
  32. package/dist/smoke.js +250 -0
  33. package/dist/task-assignment-manifest.js +96 -0
  34. package/dist/vercel-sandbox-provider.js +260 -0
  35. package/dist/workbench-cli.js +493 -0
  36. package/dist/workspace-sync.js +924 -0
  37. package/dist/workspace.js +868 -57
  38. package/package.json +4 -1
  39. package/src/artifact-archive.ts +182 -0
  40. package/src/connectors-cli.ts +115 -0
  41. package/src/credentials.ts +6 -2
  42. package/src/daytona-slice-rehearsal.ts +320 -0
  43. package/src/daytona-slice.ts +598 -0
  44. package/src/egress-allowlist-cli.ts +147 -0
  45. package/src/egress-policy.ts +188 -0
  46. package/src/git-credential.ts +155 -0
  47. package/src/github-setup.ts +366 -0
  48. package/src/help.ts +277 -21
  49. package/src/hosted-bootstrap-rehearsal.ts +312 -0
  50. package/src/hosted-bootstrap.ts +571 -0
  51. package/src/hosted-broker-server.ts +278 -0
  52. package/src/hosted-git-http-server.ts +164 -0
  53. package/src/hosted-harness-claude.ts +621 -0
  54. package/src/hosted-harness-opencode.ts +823 -0
  55. package/src/hosted-harness.ts +124 -0
  56. package/src/hosted-loop.ts +680 -0
  57. package/src/hosted-run-event-sink.ts +632 -0
  58. package/src/hosted-runtime-assets.ts +272 -0
  59. package/src/hosted-session-cli.ts +844 -0
  60. package/src/index.ts +1102 -317
  61. package/src/kill-switch-cli.ts +113 -0
  62. package/src/manifests-cli.ts +168 -0
  63. package/src/preview-runtime.ts +14 -9
  64. package/src/sandbox-provider.ts +440 -0
  65. package/src/secret-redaction.ts +124 -0
  66. package/src/session-timeline.ts +152 -0
  67. package/src/setup-prompt.ts +4 -2
  68. package/src/skill-installer.ts +13 -1
  69. package/src/smoke.ts +342 -0
  70. package/src/task-assignment-manifest.ts +164 -0
  71. package/src/vercel-sandbox-provider.ts +388 -0
  72. package/src/workbench-cli.ts +701 -0
  73. package/src/workspace-sync.ts +1148 -0
  74. package/src/workspace.ts +1051 -64
  75. package/vendor/skills/orizu-cli/SKILL.md +8 -4
  76. 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
+ }
@@ -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?.code !== 'EADDRINUSE')
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
+ }