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,272 @@
1
+ /**
2
+ * In-sandbox runtime assets for the hosted bootstrap (ALI-925, per ADR-005).
3
+ *
4
+ * Ports the DESIGN — not the code — of OpenInspect's
5
+ * `sandbox_runtime/credentials/git_credential_helper.py` into a self-contained
6
+ * script asset that Orizu writes into the sandbox and registers as a git
7
+ * `credential.helper`. On every git op git spawns the script; the script mints a
8
+ * fresh, downscoped, audited token from Orizu's per-repo broker
9
+ * (`POST /api/cli/workspaces/{id}/repo-token`) and hands it to git, caching to a
10
+ * 0600 run-scoped file with an expiry-refresh buffer. It NEVER echoes the token
11
+ * and reads the bearer only from a run-scoped 0600 file (G3): the bearer is
12
+ * never baked into git config, the clone URL, or any long-lived file.
13
+ *
14
+ * Deliberately self-contained (no `orizu` package import) so it works even when
15
+ * the CLI-install step fails — the credential path must not depend on it. The
16
+ * script is emitted as CommonJS (`.cjs`) so it runs unchanged under both `node`
17
+ * (>=18, for global `fetch`) and `bun`, whichever the sandbox image ships.
18
+ *
19
+ * The boot context is split so no secret lands in a value git persists:
20
+ * - `boot.json` — non-secret config + the PATHS of the bearer/cache files;
21
+ * - `<bearer>` — the Orizu bearer, 0600, run-scoped, reaped at teardown;
22
+ * - `<cache>` — the minted repo token, 0600, written by the script itself.
23
+ * git config holds only `credential.helper = !<runtime> <script> <boot.json>`.
24
+ */
25
+
26
+ export const HELPER_SCRIPT_BASENAME = 'orizu-git-credential-helper.cjs'
27
+ export const BOOT_CONTEXT_BASENAME = 'boot.json'
28
+ export const BEARER_BASENAME = 'bearer'
29
+ export const REPO_CRED_CACHE_BASENAME = 'repo-cred.json'
30
+
31
+ /** Refresh a cached token this long before its stated expiry (5 minutes). */
32
+ export const DEFAULT_CACHE_REFRESH_BUFFER_MS = 5 * 60 * 1000
33
+
34
+ /** Customer setup hook path, relative to the repo root (mirrors OpenInspect's
35
+ * `.openinspect/setup.sh` — fresh-boot only, non-fatal, output captured). */
36
+ export const SETUP_HOOK_RELATIVE_PATH = '.orizu/setup.sh'
37
+
38
+ // -- Pre-baked runtime marker (ALI-1017) -------------------------------------
39
+
40
+ /**
41
+ * Absolute path of the marker the pre-baked hosted-runtime image writes at build
42
+ * time (see packages/cli/hosted-runtime-image/Dockerfile). Its presence — plus
43
+ * the boot-context `prebaked` flag — tells the runtime the CLI + OpenCode are
44
+ * already on PATH, so the from-scratch install steps must be SKIPPED (they would
45
+ * otherwise fail under G5 default-deny egress).
46
+ */
47
+ export const PREBAKED_MARKER_PATH = '/opt/orizu/prebaked.json'
48
+
49
+ /**
50
+ * The schema of `/opt/orizu/prebaked.json`. Records the exact versions baked into
51
+ * the image so a run (and audit) can tell WHICH runtime it booted on. `builtFor`
52
+ * is a constant guard so a stray same-named file from another toolchain is not
53
+ * mistaken for our marker.
54
+ */
55
+ export interface PrebakedMarker {
56
+ cliVersion: string
57
+ opencodeVersion: string
58
+ claudeSdkVersion: string
59
+ builtFor: 'vercel-sandbox'
60
+ }
61
+
62
+ /**
63
+ * Parse + validate a pre-baked marker file's contents. Returns the typed marker
64
+ * when the JSON is well-formed AND carries the expected `builtFor` guard with
65
+ * string version fields; returns null otherwise (missing file → caller passes an
66
+ * empty/whitespace string; malformed JSON; wrong shape). Never throws, so a
67
+ * belt-and-braces filesystem probe can call it on best-effort file reads.
68
+ */
69
+ export function parsePrebakedMarker(raw: string): PrebakedMarker | null {
70
+ let parsed: unknown
71
+ try {
72
+ parsed = JSON.parse(raw)
73
+ } catch {
74
+ return null
75
+ }
76
+ if (!parsed || typeof parsed !== 'object') return null
77
+ const rec = parsed as Record<string, unknown>
78
+ if (rec.builtFor !== 'vercel-sandbox') return null
79
+ const { cliVersion, opencodeVersion, claudeSdkVersion } = rec
80
+ if (
81
+ typeof cliVersion !== 'string' ||
82
+ typeof opencodeVersion !== 'string' ||
83
+ typeof claudeSdkVersion !== 'string'
84
+ ) {
85
+ return null
86
+ }
87
+ return { cliVersion, opencodeVersion, claudeSdkVersion, builtFor: 'vercel-sandbox' }
88
+ }
89
+
90
+ /**
91
+ * The non-secret boot context the helper script reads. Paths are ABSOLUTE so
92
+ * the helper resolves them regardless of git's cwd at invocation time.
93
+ */
94
+ export interface HostedBootContext {
95
+ apiBaseUrl: string
96
+ workspaceId: string
97
+ sessionId: string
98
+ runId: string
99
+ sessionBranch: string
100
+ repoFullName: string
101
+ /** VCS host the helper will serve credentials for (default github.com). */
102
+ host: string
103
+ /** Absolute path to the 0600 file holding the Orizu bearer. */
104
+ bearerFile: string
105
+ /** Absolute path to the 0600 minted-token cache the script maintains. */
106
+ cacheFile: string
107
+ /**
108
+ * Broker purpose vocabulary, INJECTABLE so a later slice can flip the whole
109
+ * script from write/read to session_write/session_read without editing it. The
110
+ * helper POSTs `primary` first and falls back to `fallback` ONLY on a 403
111
+ * (unauthorized-for-primary). A 400 is treated as vocabulary drift and fails
112
+ * loudly — it means the broker does not recognise the requested purpose.
113
+ */
114
+ tokenPurposes: { primary: string; fallback: string }
115
+ /**
116
+ * Hosts (lowercased) for which the helper may serve credentials over plain
117
+ * HTTP instead of HTTPS. Production leaves this UNSET → https-only. The
118
+ * local-sim rehearsal sets it to the loopback git server's host so the
119
+ * clone-through-helper path can be proven without TLS. Secure by default.
120
+ */
121
+ insecureHttpHosts?: readonly string[]
122
+ /** Refresh buffer in ms (overridable so tests can force a refresh). */
123
+ cacheBufferMs: number
124
+ }
125
+
126
+ export function serializeBootContext(ctx: HostedBootContext): string {
127
+ return `${JSON.stringify(ctx, null, 2)}\n`
128
+ }
129
+
130
+ /**
131
+ * The git credential-helper script, as a self-contained CommonJS source string.
132
+ * Implements the `get` action (mint/emit); `store`/`erase` are no-ops because
133
+ * the broker owns credential truth and the script never persists what git hands
134
+ * back. Diagnostics go to stderr only — stdout is reserved for git's protocol.
135
+ *
136
+ * NOTE: this is a rendered ARTIFACT, exercised end-to-end as a subprocess in
137
+ * `test/cli/hosted-credential-helper.test.ts`; that subprocess test is the
138
+ * source of truth for its behavior.
139
+ */
140
+ export function renderCredentialHelperScript(): string {
141
+ return `'use strict'
142
+ // Orizu in-sandbox git credential helper (ALI-925). Self-contained CommonJS.
143
+ // Usage (git invokes it): node ${HELPER_SCRIPT_BASENAME} <boot.json> <get|store|erase>
144
+ const fs = require('node:fs')
145
+
146
+ function log(message) {
147
+ try { process.stderr.write('[orizu-cred] ' + message + '\\n') } catch (_) { /* ignore */ }
148
+ }
149
+
150
+ function readStdin() {
151
+ try { return fs.readFileSync(0, 'utf8') } catch (_) { return '' }
152
+ }
153
+
154
+ function parseProtocolInput(text) {
155
+ const out = {}
156
+ for (const raw of text.split('\\n')) {
157
+ const line = raw.replace(/\\r$/, '')
158
+ if (!line) continue
159
+ const eq = line.indexOf('=')
160
+ if (eq <= 0) continue
161
+ out[line.slice(0, eq)] = line.slice(eq + 1)
162
+ }
163
+ return out
164
+ }
165
+
166
+ function readCache(file, nowMs, bufferMs) {
167
+ let parsed
168
+ try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')) } catch (_) { return null }
169
+ if (!parsed || typeof parsed !== 'object') return null
170
+ if (!parsed.username || !parsed.password) return null
171
+ if (typeof parsed.expiresAtMs !== 'number') return null
172
+ if (parsed.expiresAtMs - nowMs <= bufferMs) return null
173
+ return parsed
174
+ }
175
+
176
+ function writeCache(file, cred) {
177
+ try {
178
+ const fd = fs.openSync(file, 'w', 0o600)
179
+ try { fs.writeSync(fd, JSON.stringify(cred)) } finally { fs.closeSync(fd) }
180
+ } catch (e) { log('cache write failed: ' + (e && e.message)) }
181
+ }
182
+
183
+ async function requestToken(url, bearer, purpose, sessionId) {
184
+ const res = await fetch(url, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + bearer },
187
+ body: JSON.stringify({ purpose: purpose, sessionId: sessionId }),
188
+ })
189
+ if (res.ok) {
190
+ const data = await res.json()
191
+ if (!data || !data.token) throw new Error('broker returned no token')
192
+ const parsedExpiry = Date.parse(data.expiresAt)
193
+ const expiresAtMs = Number.isFinite(parsedExpiry) ? parsedExpiry : Date.now() + 60 * 60 * 1000
194
+ return { cred: { username: 'x-access-token', password: String(data.token), expiresAtMs: expiresAtMs }, status: res.status }
195
+ }
196
+ return { cred: null, status: res.status }
197
+ }
198
+
199
+ async function mint(ctx) {
200
+ const bearer = fs.readFileSync(ctx.bearerFile, 'utf8').trim()
201
+ if (!bearer) throw new Error('empty bearer file')
202
+ const base = String(ctx.apiBaseUrl || '').replace(/\\/$/, '')
203
+ const url = base + '/api/cli/workspaces/' + encodeURIComponent(ctx.workspaceId) + '/repo-token'
204
+ const purposes = ctx.tokenPurposes || {}
205
+ const primary = String(purposes.primary || 'write')
206
+ const fallback = String(purposes.fallback || 'read')
207
+
208
+ // Try the primary purpose first. A 403 means "unauthorized for this purpose"
209
+ // (e.g. a read-only caller asking to write) — the ONLY legitimate fallback
210
+ // trigger. A 400 means the broker does not recognise the purpose at all
211
+ // (vocabulary drift): fail loudly rather than silently downgrade.
212
+ const first = await requestToken(url, bearer, primary, ctx.sessionId)
213
+ if (first.cred) return first.cred
214
+ if (first.status === 400) {
215
+ throw new Error('broker rejected purpose "' + primary + '" as unknown (400) — vocabulary drift')
216
+ }
217
+ if (first.status !== 403) throw new Error('broker responded ' + first.status)
218
+
219
+ const second = await requestToken(url, bearer, fallback, ctx.sessionId)
220
+ if (second.cred) return second.cred
221
+ if (second.status === 400) {
222
+ throw new Error('broker rejected fallback purpose "' + fallback + '" as unknown (400) — vocabulary drift')
223
+ }
224
+ throw new Error('broker denied purposes "' + primary + '"/"' + fallback + '" (last status ' + second.status + ')')
225
+ }
226
+
227
+ async function main() {
228
+ const contextPath = process.argv[2]
229
+ const action = process.argv[3] || 'get'
230
+ // store/erase: nothing to persist — drain stdin so git sees no SIGPIPE.
231
+ if (action !== 'get') { readStdin(); return 0 }
232
+ if (!contextPath) { log('missing boot context path'); return 1 }
233
+
234
+ let ctx
235
+ try { ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8')) } catch (e) {
236
+ log('unreadable boot context: ' + (e && e.message)); return 1
237
+ }
238
+ const input = parseProtocolInput(readStdin())
239
+
240
+ // Scope: https only (except explicitly allow-listed loopback hosts, used by the
241
+ // local-sim rehearsal), on the configured host. Empty response (exit 0) lets
242
+ // git fall through cleanly without emitting a token to the wrong remote.
243
+ const protocol = String(input.protocol || '').toLowerCase()
244
+ const host = String(input.host || '').toLowerCase()
245
+ const insecureHosts = Array.isArray(ctx.insecureHttpHosts)
246
+ ? ctx.insecureHttpHosts.map(function (h) { return String(h).toLowerCase() })
247
+ : []
248
+ const httpAllowed = protocol === 'http' && insecureHosts.indexOf(host) !== -1
249
+ if (protocol && protocol !== 'https' && !httpAllowed) { log('refusing non-https protocol'); return 0 }
250
+ const expectedHost = String(ctx.host || 'github.com').toLowerCase()
251
+ if (host && host !== expectedHost) { log('refusing host ' + host); return 0 }
252
+
253
+ const bufferMs = typeof ctx.cacheBufferMs === 'number' ? ctx.cacheBufferMs : ${DEFAULT_CACHE_REFRESH_BUFFER_MS}
254
+ let cred = readCache(ctx.cacheFile, Date.now(), bufferMs)
255
+ if (!cred) {
256
+ try { cred = await mint(ctx) } catch (e) { log('mint failed: ' + (e && e.message)); return 1 }
257
+ writeCache(ctx.cacheFile, cred)
258
+ }
259
+
260
+ for (const key of Object.keys(input)) {
261
+ if (key === 'username' || key === 'password') continue
262
+ process.stdout.write(key + '=' + input[key] + '\\n')
263
+ }
264
+ process.stdout.write('username=' + cred.username + '\\n')
265
+ process.stdout.write('password=' + cred.password + '\\n')
266
+ process.stdout.write('\\n')
267
+ return 0
268
+ }
269
+
270
+ main().then(code => process.exit(code)).catch(e => { log('fatal: ' + (e && e.message)); process.exit(1) })
271
+ `
272
+ }