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.
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 +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 +366 -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,366 @@
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
+ if (io.openUrl) {
90
+ io.openUrl(installUrl)
91
+ io.print(' Opened the install page in your browser. Waiting for you to finish...')
92
+ } else {
93
+ io.print(' Open the URL above, install the App, then return here. Waiting...')
94
+ }
95
+
96
+ const now = io.now ?? (() => Date.now())
97
+ const sleep = io.sleep ?? defaultSleep
98
+ const interval = io.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
99
+ const timeout = io.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS
100
+ const deadline = now() + timeout
101
+
102
+ for (;;) {
103
+ const poll = await fetcher(`/api/cli/github/link?state=${encodeURIComponent(state)}`)
104
+ if (poll.ok) {
105
+ const data = (await poll.json()) as { status: string; orgLogin?: string | null }
106
+ if (data.status === 'active') {
107
+ io.print(` GitHub org connected${data.orgLogin ? ` (${data.orgLogin})` : ''}.`)
108
+ return { status: 'active', orgLogin: data.orgLogin ?? null, alreadyLinked: false }
109
+ }
110
+ if (data.status === 'pending_confirmation') {
111
+ return await confirmLink(teamSlug, data.orgLogin ?? null, io, fetcher)
112
+ }
113
+ }
114
+ if (now() >= deadline) {
115
+ throw new Error(
116
+ 'Timed out waiting for the GitHub App install to complete. ' +
117
+ `Resume with \`orizu github link --team ${teamSlug}\` after finishing the install.`
118
+ )
119
+ }
120
+ await sleep(interval)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * The install callback bound an org but left the row `pending_confirmation`.
126
+ * Confirm the org with the human (second factor), then complete via the
127
+ * authenticated confirm endpoint — this is the only path to `active`.
128
+ */
129
+ async function confirmLink(
130
+ teamSlug: string,
131
+ orgLogin: string | null,
132
+ io: GithubLinkIo,
133
+ fetcher: SetupFetcher
134
+ ): Promise<GithubLinkResult> {
135
+ io.print(` GitHub reported installation on org: ${orgLogin ?? '(unknown)'}`)
136
+ if (!io.confirm) {
137
+ throw new Error(
138
+ `Org ${orgLogin ?? '(unknown)'} is bound but needs confirmation. ` +
139
+ `Run \`orizu github link --team ${teamSlug}\` interactively to confirm.`
140
+ )
141
+ }
142
+ const approved = await io.confirm(orgLogin)
143
+ if (!approved) {
144
+ throw new Error(
145
+ `Did not confirm org ${orgLogin ?? '(unknown)'}. ` +
146
+ `Re-run \`orizu github link --team ${teamSlug}\` to link the correct org.`
147
+ )
148
+ }
149
+ const resp = await fetcher('/api/cli/github/link/confirm', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({ teamSlug, orgLogin }),
153
+ })
154
+ if (!resp.ok) {
155
+ throw new Error(`Failed to confirm GitHub link: ${await readError(resp)}`)
156
+ }
157
+ const confirmed = (await resp.json()) as { orgLogin?: string | null }
158
+ io.print(` GitHub org connected (${confirmed.orgLogin ?? orgLogin ?? 'linked'}).`)
159
+ return { status: 'active', orgLogin: confirmed.orgLogin ?? orgLogin, alreadyLinked: false }
160
+ }
161
+
162
+ // -- Hosted attach ----------------------------------------------------------
163
+
164
+ export type SetupMode = 'local' | 'attach' | 'provision-then-attach'
165
+
166
+ /**
167
+ * Decide how `orizu setup` should materialize the workspace. `--local` (or the
168
+ * absence of a linked org) keeps Phase 1 scaffolding; a linked org with a
169
+ * provisioned repo attaches by clone; a linked org without a repo provisions
170
+ * first, then attaches.
171
+ */
172
+ export function decideSetupMode(input: {
173
+ local: boolean
174
+ activeInstallation: boolean
175
+ repoFullName: string | null
176
+ }): SetupMode {
177
+ if (input.local || !input.activeInstallation) return 'local'
178
+ return input.repoFullName ? 'attach' : 'provision-then-attach'
179
+ }
180
+
181
+ export interface GitRunResult {
182
+ status: number
183
+ stderr?: string
184
+ }
185
+
186
+ export interface GitRunOpts {
187
+ cwd?: string
188
+ env?: Record<string, string>
189
+ }
190
+
191
+ export type GitRunner = (args: string[], opts?: GitRunOpts) => GitRunResult
192
+
193
+ export interface HostedAttachIo {
194
+ print: (line: string) => void
195
+ fetcher?: SetupFetcher
196
+ git: GitRunner
197
+ /** Overridable for tests; defaults to creating `.orizu/` on disk. */
198
+ ensureOrizuDir?: (repoDir: string) => void
199
+ }
200
+
201
+ export interface HostedAttachOptions {
202
+ workspaceId: string
203
+ repoFullName: string | null
204
+ targetDir: string
205
+ }
206
+
207
+ export interface HostedAttachResult {
208
+ repoFullName: string
209
+ provisioned: boolean
210
+ targetDir: string
211
+ }
212
+
213
+ function gitOrThrow(git: GitRunner, args: string[], opts?: GitRunOpts) {
214
+ const result = git(args, opts)
215
+ if (result.status !== 0) {
216
+ throw new Error(`git ${args[0]} failed: ${result.stderr || `exit ${result.status}`}`)
217
+ }
218
+ }
219
+
220
+ const CREDENTIAL_HELPER = '!orizu git-credential'
221
+
222
+ /**
223
+ * Provision-if-needed, then clone with the Orizu credential helper already
224
+ * configured so the brokered token NEVER touches argv, the clone URL, or
225
+ * .git/config. The helper resolves the workspace during clone via the
226
+ * `ORIZU_WORKSPACE_ID` env var (the working tree — and orizu.team.json — does
227
+ * not exist yet), then mints on demand. Repo-local config persists the helper
228
+ * for subsequent pull/push, which resolve the workspace from cwd.
229
+ */
230
+ export async function runHostedAttach(
231
+ options: HostedAttachOptions,
232
+ io: HostedAttachIo
233
+ ): Promise<HostedAttachResult> {
234
+ const fetcher = io.fetcher ?? authedFetch
235
+ let repoFullName = options.repoFullName
236
+ let provisioned = false
237
+
238
+ if (!repoFullName) {
239
+ io.print(' Provisioning the hosted workbench repo...')
240
+ const response = await fetcher(
241
+ `/api/cli/workspaces/${encodeURIComponent(options.workspaceId)}/provision-repo`,
242
+ {
243
+ method: 'POST',
244
+ headers: { 'Content-Type': 'application/json' },
245
+ body: JSON.stringify({}),
246
+ }
247
+ )
248
+ if (!response.ok) {
249
+ throw new Error(`Failed to provision workbench repo: ${await readError(response)}`)
250
+ }
251
+ const data = (await response.json()) as { repo: string }
252
+ repoFullName = data.repo
253
+ provisioned = true
254
+ }
255
+
256
+ const cleanUrl = `https://github.com/${repoFullName}.git`
257
+
258
+ io.print(` Cloning ${repoFullName}...`)
259
+ // Clone with the helper inline + the workspace id in the env so the token is
260
+ // brokered on demand and never materializes in the URL or on disk.
261
+ gitOrThrow(
262
+ io.git,
263
+ [
264
+ '-c',
265
+ `credential.helper=${CREDENTIAL_HELPER}`,
266
+ '-c',
267
+ 'credential.useHttpPath=true',
268
+ 'clone',
269
+ cleanUrl,
270
+ options.targetDir,
271
+ ],
272
+ { env: { ORIZU_WORKSPACE_ID: options.workspaceId } }
273
+ )
274
+ // Persist the helper repo-locally for future pull/push (cwd-resolved).
275
+ gitOrThrow(io.git, ['config', 'credential.helper', CREDENTIAL_HELPER], { cwd: options.targetDir })
276
+ gitOrThrow(io.git, ['config', 'credential.useHttpPath', 'true'], { cwd: options.targetDir })
277
+
278
+ const ensureOrizuDir = io.ensureOrizuDir ?? ((repoDir: string) => mkdirSync(join(repoDir, '.orizu'), { recursive: true }))
279
+ ensureOrizuDir(options.targetDir)
280
+
281
+ io.print(' Attached. Configured the Orizu credential helper for git.')
282
+ return { repoFullName, provisioned, targetDir: options.targetDir }
283
+ }
284
+
285
+ // -- Interactive setup dispatch (ALI-996 / WS-C crumb) ----------------------
286
+
287
+ function defaultGitRunner(args: string[], opts?: { cwd?: string }): GitRunResult {
288
+ // Strip repo-context env (set by git when running inside hooks) so the
289
+ // child git is scoped strictly to cwd/args — see daytona-slice-rehearsal.ts.
290
+ const env: NodeJS.ProcessEnv = { ...process.env }
291
+ delete env.GIT_DIR
292
+ delete env.GIT_WORK_TREE
293
+ delete env.GIT_INDEX_FILE
294
+ const result = spawnSync('git', args, { cwd: opts?.cwd, encoding: 'utf8', env })
295
+ return { status: result.status ?? 1, stderr: result.stderr }
296
+ }
297
+
298
+ export interface InteractiveHostedSetupInput {
299
+ teamSlug: string
300
+ targetDir: string
301
+ /** The `--local` flag: forces Phase-1 scaffolding regardless of linkage. */
302
+ local: boolean
303
+ /** True once the required github-link step has connected an active org. */
304
+ activeInstallation: boolean
305
+ }
306
+
307
+ export interface InteractiveHostedSetupIo {
308
+ print: (line: string) => void
309
+ fetcher?: SetupFetcher
310
+ git?: GitRunner
311
+ ensureOrizuDir?: (repoDir: string) => void
312
+ }
313
+
314
+ export interface InteractiveHostedSetupResult {
315
+ mode: SetupMode
316
+ /** True when the hosted repo was cloned; false means the caller scaffolds locally. */
317
+ attached: boolean
318
+ repoFullName: string | null
319
+ }
320
+
321
+ /**
322
+ * Bridge the required github-link step to the hosted attach. Create-or-attaches
323
+ * the server workspace (whose response now surfaces repoProvider/repoFullName —
324
+ * ALI-972), then routes through the already-tested `decideSetupMode`:
325
+ * - `local` → returns `attached: false`; the caller scaffolds.
326
+ * - `attach` / `provision-then-attach` → clones (provisioning first if needed)
327
+ * via `runHostedAttach` and returns `attached: true`.
328
+ */
329
+ export async function runInteractiveHostedSetup(
330
+ input: InteractiveHostedSetupInput,
331
+ io: InteractiveHostedSetupIo
332
+ ): Promise<InteractiveHostedSetupResult> {
333
+ const fetcher = io.fetcher ?? authedFetch
334
+
335
+ const response = await fetcher('/api/cli/workspaces', {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify({ teamSlug: input.teamSlug, name: input.teamSlug, slug: input.teamSlug }),
339
+ })
340
+ if (!response.ok) {
341
+ throw new Error(`Failed to attach workspace: ${await readError(response)}`)
342
+ }
343
+ const data = (await response.json()) as {
344
+ workspace?: { id?: string; repoFullName?: string | null }
345
+ }
346
+ const workspace = data.workspace
347
+ if (!workspace?.id) {
348
+ throw new Error('Workspace attach response did not include an id')
349
+ }
350
+
351
+ const repoFullName = workspace.repoFullName ?? null
352
+ const mode = decideSetupMode({
353
+ local: input.local,
354
+ activeInstallation: input.activeInstallation,
355
+ repoFullName,
356
+ })
357
+ if (mode === 'local') {
358
+ return { mode, attached: false, repoFullName: null }
359
+ }
360
+
361
+ const result = await runHostedAttach(
362
+ { workspaceId: workspace.id, repoFullName, targetDir: input.targetDir },
363
+ { print: io.print, fetcher, git: io.git ?? defaultGitRunner, ensureOrizuDir: io.ensureOrizuDir }
364
+ )
365
+ return { mode, attached: true, repoFullName: result.repoFullName }
366
+ }