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,312 @@
1
+ /**
2
+ * Self-contained rehearsal for the hosted bootstrap (ALI-925), extending the
3
+ * ALI-973 `daytona-slice-rehearsal` pattern.
4
+ *
5
+ * The whole `bootstrapHostedSandbox` flow runs against the local-sim provider
6
+ * with NO Daytona and NO GitHub: a REAL bare git repo backs the session branch,
7
+ * and a REAL HTTP broker (`hosted-broker-server.ts`) stands in for the Orizu
8
+ * control plane (per-repo token broker + RunAPI event ingest).
9
+ *
10
+ * The broker runs as a SEPARATE PROCESS on purpose: local-sim's `exec` blocks on
11
+ * `spawnSync`, and the in-sandbox credential helper it runs calls the broker over
12
+ * the network — an in-process server could not answer while the test thread is
13
+ * blocked. The broker journals events/tokens to files the rehearsal reads back.
14
+ */
15
+
16
+ import { spawnSync } from 'child_process'
17
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
18
+ import { tmpdir } from 'os'
19
+ import { basename, join } from 'path'
20
+ import { fileURLToPath } from 'url'
21
+
22
+ import { seedLocalBareRepo, type SeededBareRepo } from './daytona-slice-rehearsal.js'
23
+ import { createLocalSimProvider, type SandboxProvider } from './sandbox-provider.js'
24
+ import type { BrokerConfig } from './hosted-broker-server.js'
25
+ import { gitHttpEnv, type GitHttpConfig } from './hosted-git-http-server.js'
26
+
27
+ /** The loopback host the git server binds; the helper is scoped to it. */
28
+ const LOOPBACK_HOST = '127.0.0.1'
29
+
30
+ function git(args: string[], cwd?: string): { stdout: string; stderr: string; status: number } {
31
+ const env: NodeJS.ProcessEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' }
32
+ delete env.GIT_DIR
33
+ delete env.GIT_WORK_TREE
34
+ delete env.GIT_INDEX_FILE
35
+ delete env.GIT_OBJECT_DIRECTORY
36
+ delete env.GIT_COMMON_DIR
37
+ const res = spawnSync('git', args, { cwd, encoding: 'utf8', env })
38
+ return { stdout: res.stdout ?? '', stderr: res.stderr ?? '', status: res.status ?? 1 }
39
+ }
40
+
41
+ function gitOrThrow(args: string[], cwd?: string): string {
42
+ const r = git(args, cwd)
43
+ if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr.trim() || `exit ${r.status}`}`)
44
+ return r.stdout
45
+ }
46
+
47
+ // -- Broker process control --------------------------------------------------
48
+
49
+ interface BunSpawnLike {
50
+ spawn: (options: {
51
+ cmd: string[]
52
+ env?: Record<string, string | undefined>
53
+ stdout?: 'pipe' | 'inherit' | 'ignore'
54
+ stderr?: 'pipe' | 'inherit' | 'ignore'
55
+ }) => { stdout: ReadableStream<Uint8Array>; kill: (signal?: number) => void }
56
+ }
57
+
58
+ function bunSpawn(): BunSpawnLike {
59
+ const b = (globalThis as { Bun?: BunSpawnLike }).Bun
60
+ if (!b?.spawn) throw new Error('Bun.spawn is required for the hosted-bootstrap rehearsal (run under bun test)')
61
+ return b
62
+ }
63
+
64
+ export interface SpawnedBroker {
65
+ apiBaseUrl: string
66
+ port: number
67
+ stop: () => void
68
+ }
69
+
70
+ /**
71
+ * Env keys the broker server reads (kept here so callers do not hand-roll them).
72
+ */
73
+ export function brokerEnv(config: BrokerConfig): Record<string, string> {
74
+ const env: Record<string, string> = { HB_BEARER: config.bearer, HB_REPO: config.repo }
75
+ if (config.eventsFile) env.HB_EVENTS_FILE = config.eventsFile
76
+ if (config.tokensFile) env.HB_TOKENS_FILE = config.tokensFile
77
+ if (config.sessionId) env.HB_SESSION_ID = config.sessionId
78
+ if (config.sessionBranch) env.HB_SESSION_BRANCH = config.sessionBranch
79
+ if (config.repoTokenStatus) env.HB_REPO_TOKEN_STATUS = String(config.repoTokenStatus)
80
+ if (config.eventsStatus) env.HB_EVENTS_STATUS = String(config.eventsStatus)
81
+ if (config.denyWrite) env.HB_DENY_WRITE = '1'
82
+ if (config.fullRunApi) env.HB_FULL_RUN_API = '1'
83
+ return env
84
+ }
85
+
86
+ /**
87
+ * Spawn the broker server as its own process and resolve once it prints
88
+ * `LISTENING <port>` on stdout.
89
+ */
90
+ export async function spawnBrokerProcess(config: BrokerConfig): Promise<SpawnedBroker> {
91
+ const brokerPath = fileURLToPath(new URL('./hosted-broker-server.ts', import.meta.url))
92
+ const proc = bunSpawn().spawn({
93
+ cmd: ['bun', brokerPath],
94
+ env: { ...process.env, ...brokerEnv(config) },
95
+ stdout: 'pipe',
96
+ stderr: 'inherit',
97
+ })
98
+ const decoder = new TextDecoder()
99
+ const reader = proc.stdout.getReader()
100
+ let buffered = ''
101
+ const deadline = Date.now() + 10_000
102
+ while (Date.now() <= deadline) {
103
+ const { done, value } = await reader.read()
104
+ if (done) break
105
+ if (value) buffered += decoder.decode(value, { stream: true })
106
+ const match = buffered.match(/LISTENING (\d+)/)
107
+ if (match) {
108
+ reader.releaseLock()
109
+ const port = Number.parseInt(match[1], 10)
110
+ return { apiBaseUrl: `http://127.0.0.1:${port}`, port, stop: () => proc.kill() }
111
+ }
112
+ }
113
+ reader.releaseLock()
114
+ proc.kill()
115
+ throw new Error('broker server did not report a listening port in time')
116
+ }
117
+
118
+ export interface SpawnedGitHttpServer {
119
+ baseUrl: string
120
+ port: number
121
+ stop: () => void
122
+ }
123
+
124
+ /**
125
+ * Spawn the authenticated smart-HTTP git server as its own process (same
126
+ * separate-process rationale as the broker) and resolve once it prints
127
+ * `LISTENING <port>`. `baseUrl` is the clone URL root; append the repo segment.
128
+ */
129
+ export async function spawnGitHttpProcess(config: GitHttpConfig): Promise<SpawnedGitHttpServer> {
130
+ const serverPath = fileURLToPath(new URL('./hosted-git-http-server.ts', import.meta.url))
131
+ const proc = bunSpawn().spawn({
132
+ cmd: ['bun', serverPath],
133
+ env: { ...process.env, ...gitHttpEnv(config) },
134
+ stdout: 'pipe',
135
+ stderr: 'inherit',
136
+ })
137
+ const decoder = new TextDecoder()
138
+ const reader = proc.stdout.getReader()
139
+ let buffered = ''
140
+ const deadline = Date.now() + 10_000
141
+ while (Date.now() <= deadline) {
142
+ const { done, value } = await reader.read()
143
+ if (done) break
144
+ if (value) buffered += decoder.decode(value, { stream: true })
145
+ const match = buffered.match(/LISTENING (\d+)/)
146
+ if (match) {
147
+ reader.releaseLock()
148
+ const port = Number.parseInt(match[1], 10)
149
+ return { baseUrl: `http://127.0.0.1:${port}`, port, stop: () => proc.kill() }
150
+ }
151
+ }
152
+ reader.releaseLock()
153
+ proc.kill()
154
+ throw new Error('git http server did not report a listening port in time')
155
+ }
156
+
157
+ // -- Rehearsal ---------------------------------------------------------------
158
+
159
+ export interface RecordedRehearsalEvent {
160
+ runId: string
161
+ eventId: string
162
+ sequence: number
163
+ eventType: string
164
+ payload: unknown
165
+ }
166
+
167
+ export interface HostedBootstrapRehearsal {
168
+ apiBaseUrl: string
169
+ bearer: string
170
+ workspaceId: string
171
+ sessionId: string
172
+ runId: string
173
+ sessionBranch: string
174
+ repoFullName: string
175
+ provider: SandboxProvider
176
+ resolveCloneUrl: (repo: string) => string
177
+ /** VCS host the helper is scoped to (the loopback git server). */
178
+ host: string
179
+ /** Loopback hosts the helper may serve over plain HTTP (rehearsal only). */
180
+ insecureHttpHosts: readonly string[]
181
+ /** All run events the broker accepted, in arrival order. */
182
+ events: () => RecordedRehearsalEvent[]
183
+ /** Every token value the broker minted (for residue-sweep assertions). */
184
+ tokensIssued: () => string[]
185
+ /** Every Basic-auth password the git server received (proves password===token). */
186
+ basicAuthPasswords: () => string[]
187
+ cleanup: () => Promise<void>
188
+ }
189
+
190
+ export interface RehearsalOptions {
191
+ bearer?: string
192
+ keep?: boolean
193
+ /** Seed `.orizu/setup.sh` onto the session branch (force-added past the
194
+ * workbench `.orizu/` gitignore) so the setup-hook step has a hook to run. */
195
+ setupHookBody?: string
196
+ /** Force the repo-token broker to a status (tests the 4xx credential path). */
197
+ repoTokenStatus?: number
198
+ /** Force the run-events ingest to a status (tests the failure path). */
199
+ eventsStatus?: number
200
+ /** Serve the full RunAPI + session + agent-token surface (P3.5 e2e). */
201
+ fullRunApi?: boolean
202
+ }
203
+
204
+ function readJsonl<T>(file: string): T[] {
205
+ let raw = ''
206
+ try {
207
+ raw = readFileSync(file, 'utf8')
208
+ } catch {
209
+ return []
210
+ }
211
+ return raw
212
+ .split('\n')
213
+ .filter(line => line.length > 0)
214
+ .map(line => JSON.parse(line) as T)
215
+ }
216
+
217
+ function readLines(file: string): string[] {
218
+ try {
219
+ return readFileSync(file, 'utf8').split('\n').filter(line => line.length > 0)
220
+ } catch {
221
+ return []
222
+ }
223
+ }
224
+
225
+ export async function createHostedBootstrapRehearsal(
226
+ options: RehearsalOptions = {}
227
+ ): Promise<HostedBootstrapRehearsal> {
228
+ const bearer = options.bearer ?? 'pat_rehearsal_bearer_do_not_log'
229
+ const base = mkdtempSync(join(tmpdir(), 'orizu-hosted-boot-'))
230
+ const repo: SeededBareRepo = seedLocalBareRepo(base, {
231
+ teamSlug: 'acme',
232
+ projectSlug: 'support-bot',
233
+ workspaceSlug: 'support-bot-hosted',
234
+ })
235
+
236
+ const workspaceId = 'ws-hosted-1'
237
+ const sessionId = 'sess-hosted-1'
238
+ const runId = 'run-hosted-1'
239
+ const sessionBranch = `orizu/session-${sessionId}`
240
+
241
+ // Cut the session branch off main on the real bare repo.
242
+ gitOrThrow(['--git-dir', repo.gitDir, 'branch', sessionBranch, repo.defaultBranch])
243
+
244
+ // Optionally seed a customer setup hook onto the session branch. `.orizu/` is
245
+ // git-ignored by the workbench contract, so the hook is force-added — an open
246
+ // question flagged in the secrets-policy doc.
247
+ if (options.setupHookBody) {
248
+ const work = mkdtempSync(join(base, 'hook-'))
249
+ gitOrThrow(['clone', '--branch', sessionBranch, repo.gitDir, work])
250
+ mkdirSync(join(work, '.orizu'), { recursive: true })
251
+ writeFileSync(join(work, '.orizu', 'setup.sh'), options.setupHookBody)
252
+ const ident = ['-c', 'user.email=hook@orizu.local', '-c', 'user.name=Hook']
253
+ gitOrThrow([...ident, '-C', work, 'add', '-f', '.orizu/setup.sh'])
254
+ gitOrThrow([...ident, '-C', work, 'commit', '-m', 'seed setup hook'])
255
+ gitOrThrow(['-C', work, 'push', 'origin', sessionBranch])
256
+ rmSync(work, { recursive: true, force: true })
257
+ }
258
+
259
+ const eventsFile = join(base, 'events.jsonl')
260
+ const tokensFile = join(base, 'tokens.txt')
261
+ const authLogFile = join(base, 'git-auth.jsonl')
262
+ writeFileSync(eventsFile, '')
263
+ writeFileSync(tokensFile, '')
264
+ writeFileSync(authLogFile, '')
265
+
266
+ const broker = await spawnBrokerProcess({
267
+ bearer,
268
+ repo: repo.repoFullName,
269
+ eventsFile,
270
+ tokensFile,
271
+ repoTokenStatus: options.repoTokenStatus,
272
+ eventsStatus: options.eventsStatus,
273
+ fullRunApi: options.fullRunApi,
274
+ sessionId,
275
+ sessionBranch,
276
+ })
277
+
278
+ // Real authenticated smart-HTTP git server on loopback: this is what makes the
279
+ // clone go git → credential helper → broker → token → Basic auth → served repo,
280
+ // instead of a filesystem clone that bypasses the helper entirely.
281
+ const gitServer = await spawnGitHttpProcess({ repoDir: repo.gitDir, authLogFile })
282
+ // git includes the (non-standard) port in the credential `host` field, so the
283
+ // helper must be scoped to `127.0.0.1:<port>`, not the bare loopback host.
284
+ const host = `${LOOPBACK_HOST}:${gitServer.port}`
285
+ const insecureHttpHosts = [host]
286
+ const cloneUrl = `${gitServer.baseUrl}/${basename(repo.gitDir)}`
287
+
288
+ const provider = createLocalSimProvider({ rootDir: join(base, 'sandboxes'), keepOnDestroy: Boolean(options.keep) })
289
+
290
+ return {
291
+ apiBaseUrl: broker.apiBaseUrl,
292
+ bearer,
293
+ workspaceId,
294
+ sessionId,
295
+ runId,
296
+ sessionBranch,
297
+ repoFullName: repo.repoFullName,
298
+ provider,
299
+ resolveCloneUrl: () => cloneUrl,
300
+ host,
301
+ insecureHttpHosts,
302
+ events: () => readJsonl<RecordedRehearsalEvent>(eventsFile),
303
+ tokensIssued: () => readLines(tokensFile),
304
+ basicAuthPasswords: () =>
305
+ readJsonl<{ user: string; pass: string }>(authLogFile).map(entry => entry.pass),
306
+ cleanup: async () => {
307
+ broker.stop()
308
+ gitServer.stop()
309
+ if (!options.keep) rmSync(base, { recursive: true, force: true })
310
+ },
311
+ }
312
+ }