orizu 0.5.7 → 0.5.9

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.
@@ -359,6 +359,12 @@ async function defaultResolveRepo(
359
359
  export async function startHostedSession(
360
360
  opts: StartHostedSessionOptions
361
361
  ): Promise<StartHostedSessionResult> {
362
+ // ALI-1035 fail-fast: a hosted run cannot reach the model provider without a key
363
+ // (the firewall brokers the real key onto egress). Refuse BEFORE creating any
364
+ // session, run row, or sandbox — no orphaned rows, no paid box, instant error.
365
+ if (!opts.modelApiKey) {
366
+ throw new Error('hosted session needs a model API key: set ANTHROPIC_API_KEY (or pass --model-key)')
367
+ }
362
368
  const rawFetch = opts.rawFetch ?? (globalThis.fetch as HostedFetch)
363
369
  const log = opts.logLine ?? ((): void => {})
364
370
  const model = opts.model ?? DEFAULT_HOSTED_MODEL
@@ -602,6 +608,32 @@ export async function startHostedSession(
602
608
  let sandboxBudgetMs = durationMs
603
609
  let tokenExpiresAtMs = now() + ttlMinutes * 60 * 1000
604
610
  let stopped = false
611
+
612
+ // ALI-1037 host-side idle gate: only KEEP a sandbox alive while its run is
613
+ // making progress. Before each keepalive extension we cheaply check whether any
614
+ // new run event landed since the last check; a run with NO new events since the
615
+ // previous rotation cycle is not extended (the in-sandbox watchdog is the
616
+ // primary stall killer — this stops the HOST from paying to keep a dead box
617
+ // alive). "Can't tell" (non-2xx / transport error) never starves a healthy run.
618
+ let lastEventCursor = 0
619
+ const hasEventProgress = async (): Promise<boolean> => {
620
+ try {
621
+ const res = await opts.fetcher(
622
+ `/api/cli/workbench-runs/${encodeURIComponent(runId)}/events?after=${lastEventCursor}&limit=500`
623
+ )
624
+ if (!res.ok) return true
625
+ const body = (await res.json().catch(() => ({}))) as { events?: unknown }
626
+ const events = Array.isArray(body.events) ? (body.events as Record<string, unknown>[]) : []
627
+ if (events.length === 0) return false
628
+ lastEventCursor = events.reduce((max, event) => {
629
+ const seq = typeof event.sequence === 'number' ? event.sequence : 0
630
+ return seq > max ? seq : max
631
+ }, lastEventCursor)
632
+ return true
633
+ } catch {
634
+ return true
635
+ }
636
+ }
605
637
  let cancelSleep: (() => void) | null = null
606
638
  const stop = (): void => {
607
639
  stopped = true
@@ -674,14 +706,21 @@ export async function startHostedSession(
674
706
  log('rotated agent bearer (file overwritten)')
675
707
  // Keepalive: extend the sandbox by one rotation interval each rotation
676
708
  // while attached (SDK supports extendTimeout(ms)), bounded by the 24h cap
677
- // so a long attached session outlives the initial --duration window.
709
+ // so a long attached session outlives the initial --duration window — BUT
710
+ // only while the run is making progress (ALI-1037). A run with no new
711
+ // events since the last rotation is not extended: the box is left to wind
712
+ // down to its current budget instead of the host paying for a stalled run.
678
713
  if (sandbox.extendTimeout && sandboxBudgetMs + rotationIntervalMs <= sandboxCapMs) {
679
- try {
680
- await sandbox.extendTimeout(rotationIntervalMs)
681
- sandboxBudgetMs += rotationIntervalMs
682
- log(`extended sandbox timeout (+${Math.round(rotationIntervalMs / 1000)}s, budget ${Math.round(sandboxBudgetMs / 60000)}m)`)
683
- } catch (error) {
684
- log(`sandbox extendTimeout failed: ${error instanceof Error ? error.message : String(error)}`)
714
+ if (!(await hasEventProgress())) {
715
+ log('sandbox timeout NOT extended: no run events since the last rotation (agent stalled) — letting the box wind down')
716
+ } else {
717
+ try {
718
+ await sandbox.extendTimeout(rotationIntervalMs)
719
+ sandboxBudgetMs += rotationIntervalMs
720
+ log(`extended sandbox timeout (+${Math.round(rotationIntervalMs / 1000)}s, budget ${Math.round(sandboxBudgetMs / 60000)}m)`)
721
+ } catch (error) {
722
+ log(`sandbox extendTimeout failed: ${error instanceof Error ? error.message : String(error)}`)
723
+ }
685
724
  }
686
725
  }
687
726
  } catch (error) {
@@ -762,7 +801,7 @@ const HOSTED_USAGE =
762
801
  '[--model <provider/model>] [--project <team/project>] [--provider vercel|local-sim] ' +
763
802
  '[--image <orizu-hosted-runtime:tag> (pre-baked VCR image; default env ORIZU_HOSTED_IMAGE)] ' +
764
803
  '[--snapshot <snapshot-id> (pre-baked Vercel snapshot, zero-Docker; default env ORIZU_HOSTED_SNAPSHOT; ' +
765
- 'mutually exclusive with --image)] [--tail]\n' +
804
+ 'mutually exclusive with --image)] [--model-key <key> (else ANTHROPIC_API_KEY; required)] [--tail]\n' +
766
805
  'Note (v0): if you disconnect, the agent token expires within its TTL (<=60m) and the run finishes ' +
767
806
  'cleanly; reconnect with `orizu run tail --run <id>`. The sandbox timeout is independent (up to 24h).'
768
807
 
@@ -821,7 +860,9 @@ export async function hostedCommand(args: string[], io: HostedCommandIo): Promis
821
860
  hostedImage: argVal(args, '--image') ?? process.env.ORIZU_HOSTED_IMAGE ?? undefined,
822
861
  hostedSnapshot: argVal(args, '--snapshot') ?? process.env.ORIZU_HOSTED_SNAPSHOT ?? undefined,
823
862
  tail,
824
- modelApiKey: process.env.ANTHROPIC_API_KEY || undefined,
863
+ // Model key: explicit --model-key wins, else ANTHROPIC_API_KEY. Falsy → the
864
+ // fail-fast in startHostedSession throws before any session/sandbox is made.
865
+ modelApiKey: argVal(args, '--model-key') ?? (process.env.ANTHROPIC_API_KEY || undefined),
825
866
  logLine: line => io.print(line),
826
867
  onTailEvent: event =>
827
868
  io.print(io.json ? JSON.stringify(event) : `#${String(event.sequence ?? '?')} ${String(event.eventType ?? '')}`),
package/src/http.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { getActiveBaseUrl, getServerCredentials, updateServerCredentials } from './credentials.js'
1
+ import {
2
+ getActiveBaseUrl,
3
+ getServerCredentials,
4
+ resolveEnvBearerToken,
5
+ updateServerCredentials,
6
+ } from './credentials.js'
2
7
  import { getFlagBaseUrl, GlobalFlags, normalizeBaseUrl } from './global-flags.js'
3
8
  import { LoginResponse, ServerCredentials, SessionServerCredentials } from './types.js'
4
9
 
@@ -122,6 +127,23 @@ async function refreshCredentials(baseUrl: string, credentials: ServerCredential
122
127
  export async function authedFetch(path: string, init: RequestInit = {}) {
123
128
  const baseUrl = resolveBaseUrl()
124
129
  assertSecureTokenTransport(baseUrl)
130
+
131
+ // In-sandbox pre-auth (ALI-1044): a bearer supplied via ORIZU_TOKEN /
132
+ // ORIZU_TOKEN_FILE takes precedence over credentials.json. It is externally
133
+ // managed (the hosted loop rotates the token file), so it is read FRESH here on
134
+ // every request and NEVER refreshed by this client — on a 401 the loop rotates
135
+ // the file and the next request naturally picks up the new bearer.
136
+ const envBearer = resolveEnvBearerToken()
137
+ if (envBearer) {
138
+ return await fetch(`${baseUrl}${path}`, {
139
+ ...init,
140
+ headers: {
141
+ ...(init.headers || {}),
142
+ Authorization: `Bearer ${envBearer}`,
143
+ },
144
+ })
145
+ }
146
+
125
147
  const credentials = getServerCredentials(baseUrl)
126
148
  if (!credentials) {
127
149
  throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
package/src/index.ts CHANGED
@@ -68,6 +68,11 @@ import { hostedCommand } from './hosted-session-cli.js'
68
68
  import { workspaceSyncCommand } from './workspace-sync.js'
69
69
  import { runGitCredential } from './git-credential.js'
70
70
  import { type GithubLinkResult, runGithubLink, runInteractiveHostedSetup } from './github-setup.js'
71
+ import {
72
+ reportCommentsCommand,
73
+ throwDeprecatedPromptCommentsCommand,
74
+ type ReportCommentsCliContext,
75
+ } from './report-comments-cli.js'
71
76
 
72
77
  function getErrorMessage(error: unknown): string {
73
78
  return error instanceof Error ? error.message : String(error)
@@ -144,60 +149,18 @@ interface PromptSummary {
144
149
  description?: string | null
145
150
  }
146
151
 
147
- interface PromptCommentAuthor {
148
- name: string
149
- initials?: string
150
- color?: string
151
- }
152
-
153
- interface PromptCommentAnchor {
154
- text: string | null
155
- startLine: number | null
156
- endLine: number | null
157
- }
158
-
159
- interface PromptCommentReply {
160
- id: string
161
- body: string
162
- author: PromptCommentAuthor
163
- createdAt: string
164
- updatedAt: string
165
- }
166
-
167
- interface PromptCommentThread {
168
- id: string
169
- status: 'open' | 'resolved'
170
- body: string
171
- anchor: PromptCommentAnchor | null
172
- author: PromptCommentAuthor
173
- createdAt: string
174
- updatedAt: string
175
- resolvedAt: string | null
176
- resolvedByUserId: string | null
177
- replyCount: number
178
- replies: PromptCommentReply[]
179
- }
180
-
181
- interface PromptCommentsPayload {
182
- prompt: {
183
- id: string
184
- name: string
185
- role: string
186
- description?: string | null
187
- }
188
- version: {
152
+ interface TaskReportPayload {
153
+ task: {
189
154
  id: string
190
- versionNumber?: number
191
- versionLabel?: string | null
155
+ title?: string | null
192
156
  status?: string | null
157
+ report: {
158
+ markdown: string
159
+ sourceName: string | null
160
+ createdAt: string | null
161
+ updatedAt?: string | null
162
+ }
193
163
  }
194
- summary: {
195
- threadCount: number
196
- openThreadCount: number
197
- resolvedThreadCount: number
198
- replyCount: number
199
- }
200
- comments: PromptCommentThread[]
201
164
  }
202
165
 
203
166
  interface ScorerSummary {
@@ -1083,78 +1046,6 @@ function printTaskStatusSummary(data: TaskStatusPayload) {
1083
1046
  }
1084
1047
  }
1085
1048
 
1086
- function compactTerminalText(value: string, maxLength = 160): string {
1087
- const compact = sanitizeTerminalText(value).replace(/\s+/g, ' ').trim()
1088
- if (compact.length <= maxLength) {
1089
- return compact
1090
- }
1091
-
1092
- return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`
1093
- }
1094
-
1095
- function printIndentedBody(value: string, indent: string) {
1096
- const lines = sanitizeTerminalText(value || '').split(/\r?\n/)
1097
- for (const line of lines) {
1098
- printLine(`${indent}${line}`)
1099
- }
1100
- }
1101
-
1102
- function formatPromptCommentAnchor(anchor: PromptCommentAnchor | null): string | null {
1103
- if (!anchor) {
1104
- return null
1105
- }
1106
-
1107
- const lineLabel = anchor.startLine && anchor.endLine
1108
- ? anchor.startLine === anchor.endLine
1109
- ? `line ${anchor.startLine}`
1110
- : `lines ${anchor.startLine}-${anchor.endLine}`
1111
- : null
1112
- const selectedText = anchor.text ? `"${compactTerminalText(anchor.text, 180)}"` : null
1113
-
1114
- if (lineLabel && selectedText) {
1115
- return `${lineLabel}: ${selectedText}`
1116
- }
1117
- return lineLabel || selectedText
1118
- }
1119
-
1120
- function printPromptComments(data: PromptCommentsPayload) {
1121
- const versionLabel = data.version.versionLabel || (
1122
- data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id
1123
- )
1124
-
1125
- printLine(`Prompt: ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`)
1126
- printLine(`Version: ${sanitizeTerminalText(String(versionLabel))} (${sanitizeTerminalText(data.version.id)})`)
1127
- printLine(
1128
- `Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
1129
- `(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`
1130
- )
1131
-
1132
- if (data.comments.length === 0) {
1133
- printLine('\nNo comments found for this prompt version.')
1134
- return
1135
- }
1136
-
1137
- for (const [index, thread] of data.comments.entries()) {
1138
- const authorName = thread.author?.name || 'Unknown'
1139
- const anchor = formatPromptCommentAnchor(thread.anchor)
1140
- printLine('')
1141
- printLine(`${index + 1}. [${thread.status}] ${sanitizeTerminalText(authorName)} · ${sanitizeTerminalText(thread.createdAt)}`)
1142
- if (anchor) {
1143
- printLine(` Selection: ${anchor}`)
1144
- }
1145
- printIndentedBody(thread.body, ' ')
1146
-
1147
- if (thread.replies.length > 0) {
1148
- printLine(` Replies (${thread.replies.length})`)
1149
- for (const reply of thread.replies) {
1150
- const replyAuthor = reply.author?.name || 'Unknown'
1151
- printLine(` - ${sanitizeTerminalText(replyAuthor)} · ${sanitizeTerminalText(reply.createdAt)}`)
1152
- printIndentedBody(reply.body, ' ')
1153
- }
1154
- }
1155
- }
1156
- }
1157
-
1158
1049
  const DEFAULT_AUTH_CALLBACK_PORT = 43123
1159
1050
 
1160
1051
  function resolveAuthCallbackPort(): number {
@@ -1562,37 +1453,6 @@ async function listJudges() {
1562
1453
  printPromptSummaries(data.judges, 'No judges found.')
1563
1454
  }
1564
1455
 
1565
- async function listPromptComments() {
1566
- const promptRef = getPositionalArg(2)
1567
- const project = getArg('--project') || await resolveProjectSlug(null)
1568
- const label = getArg('--label')
1569
- const version = getArg('--version')
1570
-
1571
- if (!promptRef) {
1572
- throw new Error('Usage: orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <version-id>] [--json]')
1573
- }
1574
- if (label && version) {
1575
- throw new Error('Use either --label or --version, not both')
1576
- }
1577
-
1578
- const params = new URLSearchParams({ project })
1579
- if (label) params.set('label', label)
1580
- if (version) params.set('version', version)
1581
-
1582
- const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}/comments?${params.toString()}`)
1583
- if (!response.ok) {
1584
- throw new Error(`Failed to fetch prompt comments: ${await response.text()}`)
1585
- }
1586
-
1587
- const data = await parseJsonResponse<PromptCommentsPayload>(response, 'Prompt comments')
1588
- if (hasJsonFlag()) {
1589
- printJson(data as unknown as Record<string, unknown>)
1590
- return
1591
- }
1592
-
1593
- printPromptComments(data)
1594
- }
1595
-
1596
1456
  async function listScorers() {
1597
1457
  const project = getArg('--project') || await resolveProjectSlug(null)
1598
1458
  const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`)
@@ -2957,6 +2817,22 @@ function printJson(value: Record<string, unknown>) {
2957
2817
  printLine(JSON.stringify(value))
2958
2818
  }
2959
2819
 
2820
+ function createReportCommentsCliContext(): ReportCommentsCliContext {
2821
+ return {
2822
+ getArg,
2823
+ getPositionalArg,
2824
+ rejectDashPrefixedOptionValue,
2825
+ resolveProjectSlug,
2826
+ readSourceFile,
2827
+ authedFetch,
2828
+ parseJsonResponse,
2829
+ hasJsonFlag,
2830
+ printJson,
2831
+ printLine,
2832
+ sanitizeTerminalText,
2833
+ }
2834
+ }
2835
+
2960
2836
  function printCapabilities() {
2961
2837
  const capabilities = getCapabilities(getCliVersion())
2962
2838
  if (hasJsonFlag()) {
@@ -4994,6 +4870,39 @@ async function setTaskReport() {
4994
4870
  )
4995
4871
  }
4996
4872
 
4873
+ async function getTaskReport() {
4874
+ const taskId = getArg('--task')
4875
+ rejectDashPrefixedOptionValue('--task', taskId)
4876
+ if (!taskId) {
4877
+ throw new Error('Usage: orizu tasks report get --task <taskId> [--json]')
4878
+ }
4879
+
4880
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`)
4881
+ if (!response.ok) {
4882
+ throw new Error(`Failed to read task report: ${await response.text()}`)
4883
+ }
4884
+
4885
+ const data = await parseJsonResponse<TaskReportPayload>(response, 'Task report get')
4886
+ if (hasJsonFlag()) {
4887
+ printJson(data as unknown as Record<string, unknown>)
4888
+ return
4889
+ }
4890
+
4891
+ const title = data.task.title ? `${data.task.title} ` : ''
4892
+ printLine(`Task report: ${sanitizeTerminalText(title)}(${sanitizeTerminalText(data.task.id)})`)
4893
+ if (data.task.status) {
4894
+ printLine(`Status: ${sanitizeTerminalText(data.task.status)}`)
4895
+ }
4896
+ if (data.task.report.sourceName) {
4897
+ printLine(`Source: ${sanitizeTerminalText(data.task.report.sourceName)}`)
4898
+ }
4899
+ if (data.task.report.createdAt) {
4900
+ printLine(`Created: ${sanitizeTerminalText(data.task.report.createdAt)}`)
4901
+ }
4902
+ printLine('')
4903
+ printLine(sanitizeTerminalText(data.task.report.markdown))
4904
+ }
4905
+
4997
4906
  interface AppDetailPayload {
4998
4907
  id: string
4999
4908
  name: string
@@ -6339,6 +6248,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
6339
6248
  return
6340
6249
  }
6341
6250
 
6251
+ if (command === 'comments') {
6252
+ await reportCommentsCommand(subcommand, createReportCommentsCliContext())
6253
+ return
6254
+ }
6255
+
6342
6256
  if (command === 'teams' && subcommand === 'list') {
6343
6257
  await listTeams()
6344
6258
  return
@@ -6383,8 +6297,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
6383
6297
  }
6384
6298
 
6385
6299
  if (command === 'prompts' && subcommand === 'comments') {
6386
- await listPromptComments()
6387
- return
6300
+ throwDeprecatedPromptCommentsCommand()
6388
6301
  }
6389
6302
 
6390
6303
  if (command === 'prompts' && subcommand === 'pull') {
@@ -6594,6 +6507,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
6594
6507
  return
6595
6508
  }
6596
6509
 
6510
+ if (command === 'tasks' && subcommand === 'report' && cliArgs[2] === 'get') {
6511
+ await getTaskReport()
6512
+ return
6513
+ }
6514
+
6597
6515
  if (command === 'tasks' && subcommand === 'pause') { await updateTaskStatus('paused'); return }
6598
6516
  if (command === 'tasks' && subcommand === 'unpause') { await updateTaskStatus('active'); return }
6599
6517
  if (command === 'tasks' && subcommand === 'complete') { await updateTaskStatus('completed'); return }
@@ -16,7 +16,7 @@ export interface ManifestsCommandIo {
16
16
  resolveProjectSlug?: (arg: string | null) => Promise<string>
17
17
  }
18
18
 
19
- const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply'])
19
+ const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply', 'revert'])
20
20
 
21
21
  async function responseMessage(response: Response): Promise<string> {
22
22
  try {
@@ -93,6 +93,28 @@ function num(value: unknown): number {
93
93
  return typeof value === 'number' ? value : 0
94
94
  }
95
95
 
96
+ // Auto-applied manifests (ALI-1040) landed by SERVER POLICY, not a human approver
97
+ // (approved_by is NULL). Mark them so a reviewer can tell policy applies from
98
+ // human applies at a glance; the reason lives in the outcome for `show`.
99
+ function wasAutoApplied(manifest: Record<string, unknown>): boolean {
100
+ return asRecord(manifest.outcome).autoApplied === true
101
+ }
102
+
103
+ /** STATUS cell: `applied*` when the server auto-applied it, else the raw status. */
104
+ function statusDisplay(manifest: Record<string, unknown>): string {
105
+ const status = typeof manifest.status === 'string' ? manifest.status : '(unknown)'
106
+ return wasAutoApplied(manifest) ? `${status}*` : status
107
+ }
108
+
109
+ /** One-line auto-apply audit note for `show`, or null when not auto-applied. */
110
+ function autoApplyNote(manifest: Record<string, unknown>): string | null {
111
+ if (!wasAutoApplied(manifest)) return null
112
+ const reason = asRecord(manifest.outcome).autoApplyReason
113
+ return typeof reason === 'string' && reason.length > 0
114
+ ? `auto-applied by policy: ${reason}`
115
+ : 'auto-applied by policy'
116
+ }
117
+
96
118
  // -- list table (ALI-1038) ----------------------------------------------------
97
119
 
98
120
  /** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
@@ -133,7 +155,7 @@ function manifestsTable(manifests: Record<string, unknown>[], now: number): stri
133
155
  if (manifests.length === 0) return 'promotion manifests\n(none)'
134
156
  const rows = [...manifests].sort(listOrder).map(manifest => ({
135
157
  id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
136
- status: typeof manifest.status === 'string' ? manifest.status : '(unknown)',
158
+ status: statusDisplay(manifest),
137
159
  type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
138
160
  changes: changesSummary(manifest),
139
161
  author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
@@ -207,7 +229,9 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
207
229
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests/${encodeURIComponent(id)}`)
208
230
  const data = await requireOk(response, 'Manifest show')
209
231
  const manifest = (data.manifest ?? {}) as Record<string, unknown>
210
- const human = manifest.actionType === 'repo_merge' ? repoMergeShow(manifest) : manifestLine(manifest)
232
+ const base = manifest.actionType === 'repo_merge' ? repoMergeShow(manifest) : manifestLine(manifest)
233
+ const note = autoApplyNote(manifest)
234
+ const human = note ? `${base}\n ${note}` : base
211
235
  emit(io, { manifest }, human)
212
236
  return 0
213
237
  }
@@ -244,6 +268,6 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
244
268
  return 0
245
269
  }
246
270
 
247
- io.print('Usage: orizu manifests <list|show|approve|reject|apply> ... [--json]')
271
+ io.print('Usage: orizu manifests <list|show|approve|reject|apply|revert> ... [--json]')
248
272
  return 1
249
273
  }