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.
- 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 +276 -21
- 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 +945 -258
- 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 +96 -0
- 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 +277 -21
- 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 +1102 -317
- 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 +164 -0
- 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 +8 -4
- 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
|
+
}
|