orizu 0.4.1 → 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 +271 -16
- 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 +855 -237
- 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 +39 -2
- 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 +272 -16
- 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 +987 -296
- 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 +70 -2
- 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 +3 -1
- package/vendor/skills/orizu-cli/references/cli-reference.md +3 -3
|
@@ -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
|
+
}
|