orizu 0.4.1 → 0.5.1

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 +248 -0
  11. package/dist/help.js +271 -16
  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 +855 -237
  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 +39 -2
  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 +396 -0
  48. package/src/help.ts +272 -16
  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 +987 -296
  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 +70 -2
  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 +3 -1
  76. package/vendor/skills/orizu-cli/references/cli-reference.md +3 -3
@@ -0,0 +1,396 @@
1
+ /**
2
+ * GitHub link + hosted-repo attach for `orizu setup` (ALI-971 / WS-C + WS-D).
3
+ *
4
+ * Pure logic + injected HTTP/git/io so index.ts stays thin and the flows are
5
+ * unit-testable without a real browser, server, or git. Two responsibilities:
6
+ *
7
+ * 1. `runGithubLink` — the REQUIRED team-setup link step: POST the link route,
8
+ * open/print the App-install URL, then poll until the org is connected
9
+ * (with a timeout + resume guidance).
10
+ * 2. `runHostedAttach` — clone the provisioned workbench repo and wire the
11
+ * repo-local git credential helper so future pull/push flow through the
12
+ * Orizu broker. Never stores a long-lived credential: the clone uses a
13
+ * one-shot brokered token embedded only in the transient clone URL, which
14
+ * is immediately rewritten to a clean origin.
15
+ */
16
+
17
+ import { spawnSync } from 'child_process'
18
+ import { mkdirSync } from 'fs'
19
+ import { join } from 'path'
20
+
21
+ import { authedFetch } from './http.js'
22
+
23
+ export type SetupFetcher = (path: string, init?: RequestInit) => Promise<Response>
24
+
25
+ export interface GithubLinkIo {
26
+ print: (line: string) => void
27
+ fetcher?: SetupFetcher
28
+ openUrl?: (url: string) => void
29
+ sleep?: (ms: number) => Promise<void>
30
+ now?: () => number
31
+ pollIntervalMs?: number
32
+ pollTimeoutMs?: number
33
+ /**
34
+ * Second-factor confirmation of the bound org (confused-deputy defense). The
35
+ * callback binds the installation but parks it in `pending_confirmation`; this
36
+ * asks the human to confirm the org they were shown before it becomes active.
37
+ */
38
+ confirm?: (orgLogin: string | null) => Promise<boolean>
39
+ }
40
+
41
+ export interface GithubLinkResult {
42
+ status: 'active'
43
+ orgLogin: string | null
44
+ alreadyLinked: boolean
45
+ }
46
+
47
+ const DEFAULT_POLL_INTERVAL_MS = 2000
48
+ const DEFAULT_POLL_TIMEOUT_MS = 180000
49
+
50
+ function defaultSleep(ms: number): Promise<void> {
51
+ return new Promise(resolve => setTimeout(resolve, ms))
52
+ }
53
+
54
+ async function readError(response: Response): Promise<string> {
55
+ try {
56
+ const body = (await response.json()) as { error?: string }
57
+ if (body.error) return body.error
58
+ } catch {
59
+ // fall through to status text
60
+ }
61
+ return `status ${response.status}`
62
+ }
63
+
64
+ /**
65
+ * Begin (or resume) the required GitHub link for a team and block until the
66
+ * customer org is connected. Throws with resume guidance on timeout.
67
+ */
68
+ export async function runGithubLink(teamSlug: string, io: GithubLinkIo): Promise<GithubLinkResult> {
69
+ const fetcher = io.fetcher ?? authedFetch
70
+
71
+ const start = await fetcher('/api/cli/github/link', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ teamSlug }),
75
+ })
76
+
77
+ if (start.status === 409) {
78
+ io.print(' GitHub org already connected for this team.')
79
+ return { status: 'active', orgLogin: null, alreadyLinked: true }
80
+ }
81
+ if (!start.ok) {
82
+ throw new Error(`Failed to start GitHub link: ${await readError(start)}`)
83
+ }
84
+
85
+ const { installUrl, state } = (await start.json()) as { installUrl: string; state: string }
86
+
87
+ io.print(' Connect your team GitHub org by installing the Orizu GitHub App:')
88
+ io.print(` ${installUrl}`)
89
+ // Bug 2: personal accounts cannot host an auto-provisioned workbench.
90
+ io.print(' • Install into a GitHub ORGANIZATION, not a personal account. Personal')
91
+ io.print(' accounts are not supported for auto-provisioning; creating an org is free.')
92
+ // Bug 4: repo selection guidance — GitHub forces picking at least one repo.
93
+ io.print(' • On the "Repository access" step choose "Only select repositories" and pick')
94
+ io.print(' any one repo (GitHub requires at least one). Orizu adds the workbench repo')
95
+ io.print(' it creates automatically — do NOT choose "All repositories".')
96
+ if (io.openUrl) {
97
+ io.openUrl(installUrl)
98
+ io.print(' Opened the install page in your browser.')
99
+ } else {
100
+ io.print(' Open the URL above and install the App, then return here.')
101
+ }
102
+ io.print(
103
+ ` Waiting for you to finish installing in the browser… (Ctrl+C to abort; resume later with \`orizu github link --team ${teamSlug}\`).`
104
+ )
105
+
106
+ const now = io.now ?? (() => Date.now())
107
+ const sleep = io.sleep ?? defaultSleep
108
+ const interval = io.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
109
+ const timeout = io.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS
110
+ const deadline = now() + timeout
111
+ // Periodic heartbeat so a long install does not look hung; never echoes state.
112
+ const HEARTBEAT_MS = 15000
113
+ let lastHeartbeat = now()
114
+
115
+ for (;;) {
116
+ const poll = await fetcher(`/api/cli/github/link?state=${encodeURIComponent(state)}`)
117
+ if (poll.ok) {
118
+ const data = (await poll.json()) as {
119
+ status: string
120
+ orgLogin?: string | null
121
+ error?: string
122
+ }
123
+ if (data.status === 'active') {
124
+ io.print(` GitHub org connected${data.orgLogin ? ` (${data.orgLogin})` : ''}.`)
125
+ return { status: 'active', orgLogin: data.orgLogin ?? null, alreadyLinked: false }
126
+ }
127
+ if (data.status === 'pending_confirmation') {
128
+ return await confirmLink(teamSlug, data.orgLogin ?? null, io, fetcher)
129
+ }
130
+ // Distinguishable terminal errors (personal account / expired nonce): print
131
+ // the server's reason and exit non-zero instead of polling until timeout.
132
+ if (data.status === 'unsupported_account' || data.status === 'expired') {
133
+ throw new Error(
134
+ data.error ??
135
+ `GitHub link could not be completed. Re-run \`orizu github link --team ${teamSlug}\`.`
136
+ )
137
+ }
138
+ // status 'pending' → keep waiting.
139
+ }
140
+ if (now() >= deadline) {
141
+ throw new Error(
142
+ 'Timed out waiting for the GitHub App install to complete. ' +
143
+ `Resume with \`orizu github link --team ${teamSlug}\` after finishing the install.`
144
+ )
145
+ }
146
+ if (now() - lastHeartbeat >= HEARTBEAT_MS) {
147
+ io.print(' …still waiting for the GitHub install to complete.')
148
+ lastHeartbeat = now()
149
+ }
150
+ await sleep(interval)
151
+ }
152
+ }
153
+
154
+ /**
155
+ * The install callback bound an org but left the row `pending_confirmation`.
156
+ * Confirm the org with the human (second factor), then complete via the
157
+ * authenticated confirm endpoint — this is the only path to `active`.
158
+ */
159
+ async function confirmLink(
160
+ teamSlug: string,
161
+ orgLogin: string | null,
162
+ io: GithubLinkIo,
163
+ fetcher: SetupFetcher
164
+ ): Promise<GithubLinkResult> {
165
+ io.print(` GitHub reported installation on org: ${orgLogin ?? '(unknown)'}`)
166
+ if (!io.confirm) {
167
+ throw new Error(
168
+ `Org ${orgLogin ?? '(unknown)'} is bound but needs confirmation. ` +
169
+ `Run \`orizu github link --team ${teamSlug}\` interactively to confirm.`
170
+ )
171
+ }
172
+ const approved = await io.confirm(orgLogin)
173
+ if (!approved) {
174
+ throw new Error(
175
+ `Did not confirm org ${orgLogin ?? '(unknown)'}. ` +
176
+ `Re-run \`orizu github link --team ${teamSlug}\` to link the correct org.`
177
+ )
178
+ }
179
+ const resp = await fetcher('/api/cli/github/link/confirm', {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({ teamSlug, orgLogin }),
183
+ })
184
+ if (!resp.ok) {
185
+ throw new Error(`Failed to confirm GitHub link: ${await readError(resp)}`)
186
+ }
187
+ const confirmed = (await resp.json()) as { orgLogin?: string | null }
188
+ io.print(` GitHub org connected (${confirmed.orgLogin ?? orgLogin ?? 'linked'}).`)
189
+ return { status: 'active', orgLogin: confirmed.orgLogin ?? orgLogin, alreadyLinked: false }
190
+ }
191
+
192
+ // -- Hosted attach ----------------------------------------------------------
193
+
194
+ export type SetupMode = 'local' | 'attach' | 'provision-then-attach'
195
+
196
+ /**
197
+ * Decide how `orizu setup` should materialize the workspace. `--local` (or the
198
+ * absence of a linked org) keeps Phase 1 scaffolding; a linked org with a
199
+ * provisioned repo attaches by clone; a linked org without a repo provisions
200
+ * first, then attaches.
201
+ */
202
+ export function decideSetupMode(input: {
203
+ local: boolean
204
+ activeInstallation: boolean
205
+ repoFullName: string | null
206
+ }): SetupMode {
207
+ if (input.local || !input.activeInstallation) return 'local'
208
+ return input.repoFullName ? 'attach' : 'provision-then-attach'
209
+ }
210
+
211
+ export interface GitRunResult {
212
+ status: number
213
+ stderr?: string
214
+ }
215
+
216
+ export interface GitRunOpts {
217
+ cwd?: string
218
+ env?: Record<string, string>
219
+ }
220
+
221
+ export type GitRunner = (args: string[], opts?: GitRunOpts) => GitRunResult
222
+
223
+ export interface HostedAttachIo {
224
+ print: (line: string) => void
225
+ fetcher?: SetupFetcher
226
+ git: GitRunner
227
+ /** Overridable for tests; defaults to creating `.orizu/` on disk. */
228
+ ensureOrizuDir?: (repoDir: string) => void
229
+ }
230
+
231
+ export interface HostedAttachOptions {
232
+ workspaceId: string
233
+ repoFullName: string | null
234
+ targetDir: string
235
+ }
236
+
237
+ export interface HostedAttachResult {
238
+ repoFullName: string
239
+ provisioned: boolean
240
+ targetDir: string
241
+ }
242
+
243
+ function gitOrThrow(git: GitRunner, args: string[], opts?: GitRunOpts) {
244
+ const result = git(args, opts)
245
+ if (result.status !== 0) {
246
+ throw new Error(`git ${args[0]} failed: ${result.stderr || `exit ${result.status}`}`)
247
+ }
248
+ }
249
+
250
+ const CREDENTIAL_HELPER = '!orizu git-credential'
251
+
252
+ /**
253
+ * Provision-if-needed, then clone with the Orizu credential helper already
254
+ * configured so the brokered token NEVER touches argv, the clone URL, or
255
+ * .git/config. The helper resolves the workspace during clone via the
256
+ * `ORIZU_WORKSPACE_ID` env var (the working tree — and orizu.team.json — does
257
+ * not exist yet), then mints on demand. Repo-local config persists the helper
258
+ * for subsequent pull/push, which resolve the workspace from cwd.
259
+ */
260
+ export async function runHostedAttach(
261
+ options: HostedAttachOptions,
262
+ io: HostedAttachIo
263
+ ): Promise<HostedAttachResult> {
264
+ const fetcher = io.fetcher ?? authedFetch
265
+ let repoFullName = options.repoFullName
266
+ let provisioned = false
267
+
268
+ if (!repoFullName) {
269
+ io.print(' Provisioning the hosted workbench repo...')
270
+ const response = await fetcher(
271
+ `/api/cli/workspaces/${encodeURIComponent(options.workspaceId)}/provision-repo`,
272
+ {
273
+ method: 'POST',
274
+ headers: { 'Content-Type': 'application/json' },
275
+ body: JSON.stringify({}),
276
+ }
277
+ )
278
+ if (!response.ok) {
279
+ throw new Error(`Failed to provision workbench repo: ${await readError(response)}`)
280
+ }
281
+ const data = (await response.json()) as { repo: string }
282
+ repoFullName = data.repo
283
+ provisioned = true
284
+ }
285
+
286
+ const cleanUrl = `https://github.com/${repoFullName}.git`
287
+
288
+ io.print(` Cloning ${repoFullName}...`)
289
+ // Clone with the helper inline + the workspace id in the env so the token is
290
+ // brokered on demand and never materializes in the URL or on disk.
291
+ gitOrThrow(
292
+ io.git,
293
+ [
294
+ '-c',
295
+ `credential.helper=${CREDENTIAL_HELPER}`,
296
+ '-c',
297
+ 'credential.useHttpPath=true',
298
+ 'clone',
299
+ cleanUrl,
300
+ options.targetDir,
301
+ ],
302
+ { env: { ORIZU_WORKSPACE_ID: options.workspaceId } }
303
+ )
304
+ // Persist the helper repo-locally for future pull/push (cwd-resolved).
305
+ gitOrThrow(io.git, ['config', 'credential.helper', CREDENTIAL_HELPER], { cwd: options.targetDir })
306
+ gitOrThrow(io.git, ['config', 'credential.useHttpPath', 'true'], { cwd: options.targetDir })
307
+
308
+ const ensureOrizuDir = io.ensureOrizuDir ?? ((repoDir: string) => mkdirSync(join(repoDir, '.orizu'), { recursive: true }))
309
+ ensureOrizuDir(options.targetDir)
310
+
311
+ io.print(' Attached. Configured the Orizu credential helper for git.')
312
+ return { repoFullName, provisioned, targetDir: options.targetDir }
313
+ }
314
+
315
+ // -- Interactive setup dispatch (ALI-996 / WS-C crumb) ----------------------
316
+
317
+ function defaultGitRunner(args: string[], opts?: { cwd?: string }): GitRunResult {
318
+ // Strip repo-context env (set by git when running inside hooks) so the
319
+ // child git is scoped strictly to cwd/args — see daytona-slice-rehearsal.ts.
320
+ const env: NodeJS.ProcessEnv = { ...process.env }
321
+ delete env.GIT_DIR
322
+ delete env.GIT_WORK_TREE
323
+ delete env.GIT_INDEX_FILE
324
+ const result = spawnSync('git', args, { cwd: opts?.cwd, encoding: 'utf8', env })
325
+ return { status: result.status ?? 1, stderr: result.stderr }
326
+ }
327
+
328
+ export interface InteractiveHostedSetupInput {
329
+ teamSlug: string
330
+ targetDir: string
331
+ /** The `--local` flag: forces Phase-1 scaffolding regardless of linkage. */
332
+ local: boolean
333
+ /** True once the required github-link step has connected an active org. */
334
+ activeInstallation: boolean
335
+ }
336
+
337
+ export interface InteractiveHostedSetupIo {
338
+ print: (line: string) => void
339
+ fetcher?: SetupFetcher
340
+ git?: GitRunner
341
+ ensureOrizuDir?: (repoDir: string) => void
342
+ }
343
+
344
+ export interface InteractiveHostedSetupResult {
345
+ mode: SetupMode
346
+ /** True when the hosted repo was cloned; false means the caller scaffolds locally. */
347
+ attached: boolean
348
+ repoFullName: string | null
349
+ }
350
+
351
+ /**
352
+ * Bridge the required github-link step to the hosted attach. Create-or-attaches
353
+ * the server workspace (whose response now surfaces repoProvider/repoFullName —
354
+ * ALI-972), then routes through the already-tested `decideSetupMode`:
355
+ * - `local` → returns `attached: false`; the caller scaffolds.
356
+ * - `attach` / `provision-then-attach` → clones (provisioning first if needed)
357
+ * via `runHostedAttach` and returns `attached: true`.
358
+ */
359
+ export async function runInteractiveHostedSetup(
360
+ input: InteractiveHostedSetupInput,
361
+ io: InteractiveHostedSetupIo
362
+ ): Promise<InteractiveHostedSetupResult> {
363
+ const fetcher = io.fetcher ?? authedFetch
364
+
365
+ const response = await fetcher('/api/cli/workspaces', {
366
+ method: 'POST',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({ teamSlug: input.teamSlug, name: input.teamSlug, slug: input.teamSlug }),
369
+ })
370
+ if (!response.ok) {
371
+ throw new Error(`Failed to attach workspace: ${await readError(response)}`)
372
+ }
373
+ const data = (await response.json()) as {
374
+ workspace?: { id?: string; repoFullName?: string | null }
375
+ }
376
+ const workspace = data.workspace
377
+ if (!workspace?.id) {
378
+ throw new Error('Workspace attach response did not include an id')
379
+ }
380
+
381
+ const repoFullName = workspace.repoFullName ?? null
382
+ const mode = decideSetupMode({
383
+ local: input.local,
384
+ activeInstallation: input.activeInstallation,
385
+ repoFullName,
386
+ })
387
+ if (mode === 'local') {
388
+ return { mode, attached: false, repoFullName: null }
389
+ }
390
+
391
+ const result = await runHostedAttach(
392
+ { workspaceId: workspace.id, repoFullName, targetDir: input.targetDir },
393
+ { print: io.print, fetcher, git: io.git ?? defaultGitRunner, ensureOrizuDir: io.ensureOrizuDir }
394
+ )
395
+ return { mode, attached: true, repoFullName: result.repoFullName }
396
+ }