@agent-compose/sdk 0.8.1 → 0.8.3

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 (48) hide show
  1. package/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
  2. package/dist/agent/agent-context.d.ts +1 -1
  3. package/dist/agent/agent-loop.d.ts +5 -1
  4. package/dist/agent/desktop-open.d.ts +184 -0
  5. package/dist/agent/perf-sampler.d.ts +99 -0
  6. package/dist/agent/services-manifest.d.ts +88 -0
  7. package/dist/agent/services-restore.d.ts +58 -0
  8. package/dist/client.d.ts +189 -15
  9. package/dist/display.d.ts +17 -0
  10. package/dist/index.d.ts +14 -5
  11. package/dist/index.js +1625 -120
  12. package/dist/runtimes/_cli-agent.d.ts +372 -2
  13. package/dist/runtimes/claude-code.d.ts +12 -0
  14. package/dist/runtimes/codex.buildcommand.test.d.ts +9 -0
  15. package/dist/runtimes/codex.d.ts +8 -0
  16. package/dist/runtimes/openai-desktop.js +1555 -120
  17. package/dist/runtimes/session-env.test.d.ts +14 -0
  18. package/dist/sandbox/sizes.d.ts +120 -30
  19. package/dist/sandbox.d.ts +1 -1
  20. package/dist/types/api-conversations.d.ts +476 -1
  21. package/dist/types/api-factory.d.ts +164 -7
  22. package/dist/types/api-runs.d.ts +23 -1
  23. package/dist/types/protocol.d.ts +32 -1
  24. package/dist/types/runtime.d.ts +120 -0
  25. package/dist/types/workflow-metadata.d.ts +6 -5
  26. package/package.json +1 -1
  27. package/src/agent/agent-context.ts +128 -28
  28. package/src/agent/agent-loop.ts +10 -3
  29. package/src/agent/desktop-open.ts +418 -0
  30. package/src/agent/perf-sampler.ts +202 -0
  31. package/src/agent/services-manifest.ts +356 -0
  32. package/src/agent/services-restore.ts +195 -0
  33. package/src/client.ts +384 -32
  34. package/src/display.ts +44 -1
  35. package/src/index.ts +74 -7
  36. package/src/runtimes/_cli-agent.ts +1160 -67
  37. package/src/runtimes/claude-code.ts +187 -12
  38. package/src/runtimes/codex.ts +65 -2
  39. package/src/sandbox/providers/e2b.ts +8 -4
  40. package/src/sandbox/providers/local.ts +16 -4
  41. package/src/sandbox/sizes.ts +127 -44
  42. package/src/sandbox.ts +8 -0
  43. package/src/types/api-conversations.ts +461 -2
  44. package/src/types/api-factory.ts +165 -7
  45. package/src/types/api-runs.ts +25 -1
  46. package/src/types/protocol.ts +30 -1
  47. package/src/types/runtime.ts +122 -0
  48. package/src/types/workflow-metadata.ts +6 -5
package/src/client.ts CHANGED
@@ -23,7 +23,7 @@ import type {
23
23
  RunStatus, ResumePauseOptions, ResumePauseResponse, AnswerSteerOptions,
24
24
  RequestAgentPauseOptions, RequestAgentPauseResponse,
25
25
  SendAgentMessageOptions, SendAgentMessageResponse,
26
- RunDetail, RunListEntry, ListRunsOptions, TimelineEvent, RunFundingResponse, EventRow, ReportEventInput,
26
+ RunDetail, RunListEntry, ListRunsOptions, TimelineEvent, RunFundingResponse, RunStepUsageResponse, EventRow, ReportEventInput,
27
27
  ListEventsOptions, ListEventsResult, RunArtifactRow, RunLogLine, ListRunLogsOptions,
28
28
  CancelRunResponse, ListSnapshotsOptions, SnapshotListResponse, SnapshotListEntry, RunSnapshotEntry,
29
29
  } from "./types/api-runs.js";
@@ -31,11 +31,15 @@ import type {
31
31
  TeamMember, Mention, CreateMentionsInput,
32
32
  ConversationsPage, SessionsPage, ConversationDetail, ConversationThread,
33
33
  CreateCloudSessionInput, CloudSessionCreated,
34
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
35
- SessionChangeSet, SessionMergeReport, SessionDiscardReport,
34
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
35
+ SessionChangeSet, SessionMergeGated, SessionMergeReport, SessionDiscardReport,
36
+ SessionChangePreflight, SessionRebaseReport, SessionAutoRebaseState,
37
+ ReviewSuggestionInput, ReviewNotesPublished, ReviewGitCredential,
36
38
  SendConversationMessageInput, SendConversationMessageResult,
37
39
  ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
40
+ SessionDirectMessageSent, BranchClaimGranted, BranchClaimsReleased, BranchClaimState,
38
41
  ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
42
+ SessionPerfHistoryPage,
39
43
  } from "./types/api-conversations.js";
40
44
  import type {
41
45
  ConversationMemberRole, ConversationMember, ArtifactScope,
@@ -49,10 +53,11 @@ import type {
49
53
  import type {
50
54
  RegisterResult, RegisterWorkflowInput, TemplateRow, TemplateDetail, ListTemplatesOptions,
51
55
  SearchFactoryFilesOptions, FactoryFileSearchResult, FactoryFileWriteResult,
52
- FactoryRow, CreateFactoryInput, UpdateFactoryInput,
56
+ FactoryFileListRow, ListFactoryFilesOptions, FactoryFileListPage,
57
+ FactoryRow, CreateFactoryInput, UpdateFactoryInput, FactoryPerfSummary,
53
58
  ScheduleRow, CreateScheduleInput,
54
- SecretOptions, SetSecretResult, SecretListEntry,
55
- CreateApiKeyInput, ApiKey, ApiKeyCreated, UsageResponse,
59
+ SecretOptions, SetSecretResult, SecretListEntry, SessionSecretEntry, SessionSecretInput, SessionSecretRequestCreated, SessionSecretRequestStatus,
60
+ CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage, UsageResponse,
56
61
  DriveRepoLink, CreateDriveRepoLinkInput,
57
62
  DriveMountSession, CreateDriveMountSessionInput,
58
63
  } from "./types/api-factory.js";
@@ -73,6 +78,7 @@ export type {
73
78
  SendAgentMessageOptions, SendAgentMessageResponse,
74
79
  RunDetail, RunListEntry, ListRunsOptions, TimelineEvent,
75
80
  FundingLane, RunFundingStamp, RunFundingUsageRow, RunFundingResponse,
81
+ RunStepUsage, RunStepUsageResponse,
76
82
  EventSubjectType, EventRow, RunArtifactRow, ReportEventInput, ListEventsOptions, ListEventsResult,
77
83
  RunLogLine, ListRunLogsOptions, CancelRunResponse,
78
84
  ListSnapshotsOptions, SnapshotListEntry, SnapshotListResponse, RunSnapshotEntry,
@@ -82,12 +88,17 @@ export type {
82
88
  ConversationMessagePart, ConversationRow, ConversationMessageRow,
83
89
  ConversationsPage, SessionsPage, ConversationDetail,
84
90
  CreateCloudSessionInput, CloudSessionCreated, ConversationThread,
85
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
86
- SessionFileChange, SessionChangeSet,
87
- SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
91
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
92
+ SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion,
93
+ ReviewSuggestionInput, ReviewNotesPublished, ReviewGitCredential, SessionReviewDocument,
94
+ ReviewSuggestionDecision, ReviewSuggestionDecisions,
95
+ SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport,
96
+ SessionPreflightFileRow, SessionChangePreflight, SessionRebaseReport, SessionAutoRebaseState,
88
97
  ConversationPageContext, SendConversationMessageInput, ConversationTurnState,
89
- SendConversationMessageResult, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
98
+ SendConversationMessageResult, UnnotifiedMention, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
90
99
  ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
100
+ SessionDirectMessageSent, BranchClaimInfo, BranchClaimGranted, BranchClaimsReleased, BranchClaimState,
101
+ SessionPerfBucket, SessionPerfHistoryPage,
91
102
  } from "./types/api-conversations.js";
92
103
  export type {
93
104
  ConversationMemberRole, ConversationMember,
@@ -104,11 +115,12 @@ export type {
104
115
  RegisterResult, RegisteredRuntime, RuntimeSourceInput, TemplateSourceRef, RegisterWorkflowInput,
105
116
  TemplateRow, TemplateDetail, ListTemplatesOptions,
106
117
  FactoryFileSearchRow, FactoryFolderSearchRow, SearchFactoryFilesOptions, FactoryFileSearchResult, FactoryFileWriteResult,
118
+ FactoryFileListRow, ListFactoryFilesOptions, FactoryFileListPage,
107
119
  PublicFileLinkState,
108
- FactoryRow, CreateFactoryInput, UpdateFactoryInput,
120
+ FactoryRow, CreateFactoryInput, UpdateFactoryInput, FactoryPerfSummary,
109
121
  ScheduleRow, CreateScheduleInput,
110
- SecretOptions, SetSecretResult, SecretListEntry,
111
- CreateApiKeyInput, ApiKey, ApiKeyCreated,
122
+ SecretOptions, SetSecretResult, SecretListEntry, SessionSecretEntry, SessionSecretInput, SessionSecretRequestCreated, SessionSecretRequestStatus,
123
+ CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage,
112
124
  UsageRollupRow, UsageResponse,
113
125
  DriveRepoLink, CreateDriveRepoLinkInput,
114
126
  DriveMountSession, CreateDriveMountSessionInput,
@@ -164,6 +176,17 @@ interface FactoryFileWriteWire {
164
176
  created: boolean;
165
177
  }
166
178
 
179
+ /** `GET /factories/:slug/files` — the rows are camelCase on the wire
180
+ * already; only the envelope is snake_case. `at` appears exactly when the
181
+ * request carried `?at=`. */
182
+ interface FactoryFileListWire {
183
+ object: "list";
184
+ data: FactoryFileListRow[];
185
+ has_more: boolean;
186
+ next_cursor: string | null;
187
+ at?: { head: string; committed_at: string | null };
188
+ }
189
+
167
190
  export interface AgentComposeClientOptions {
168
191
  /** Your team's API key — minted from the dashboard or `agentc keys create`.
169
192
  * Required. Resolved from `process.env.AGENT_COMPOSE_API_KEY` when omitted. */
@@ -652,6 +675,16 @@ export class AgentComposeClient {
652
675
  );
653
676
  }
654
677
 
678
+ /** Attested platform tokens attributed to each of a run's steps, derived
679
+ * server-side from the spend ledger by step time window. Empty `steps` is
680
+ * an honest answer (legacy run, or funded outside the platform lane —
681
+ * that money is never metered). */
682
+ async getRunStepUsage(runId: string): Promise<RunStepUsageResponse> {
683
+ return this.fetch<RunStepUsageResponse>(
684
+ `/api/v1/workflows/${encodeURIComponent(runId)}/step-usage`,
685
+ );
686
+ }
687
+
655
688
  /** Report a durable event against a run. Events are late-binding facts
656
689
  * like quality.accepted, defect.regression, or intervention.override. */
657
690
  async reportEvent(runId: string, input: ReportEventInput): Promise<EventRow> {
@@ -744,7 +777,7 @@ export class AgentComposeClient {
744
777
  * U+FFFD and the original bytes are unrecoverable). */
745
778
  async getFactoryFileBytes(
746
779
  path: string,
747
- opts?: { factorySlug?: string; revision?: number; branch?: string },
780
+ opts?: { factorySlug?: string; revision?: number; branch?: string; at?: string },
748
781
  ): Promise<Uint8Array> {
749
782
  const factorySlug = opts?.factorySlug
750
783
  ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
@@ -755,6 +788,10 @@ export class AgentComposeClient {
755
788
  // session's own `session-<uuid>` branch, where its unmerged work lives.
756
789
  // Mutually exclusive with `revision` (the server rejects the combination).
757
790
  if (opts?.branch !== undefined) q.set("branch", opts.branch);
791
+ // Browse-at-head (read-only): the file AS OF a retained 64-hex commit of
792
+ // the MAIN drive. A garbage-collected/unknown head answers 410
793
+ // `at_unavailable`.
794
+ if (opts?.at !== undefined) q.set("at", opts.at);
758
795
  const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
759
796
  `/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q}`,
760
797
  { responseType: "arrayBuffer" },
@@ -767,7 +804,7 @@ export class AgentComposeClient {
767
804
  * text corrupts the bytes irreversibly. */
768
805
  async getFactoryFile(
769
806
  path: string,
770
- opts?: { factorySlug?: string; revision?: number; branch?: string },
807
+ opts?: { factorySlug?: string; revision?: number; branch?: string; at?: string },
771
808
  ): Promise<string> {
772
809
  return new TextDecoder().decode(await this.getFactoryFileBytes(path, opts));
773
810
  }
@@ -840,6 +877,31 @@ export class AgentComposeClient {
840
877
  return this.fetch<SessionsPage>(`/api/v1/sessions?${q}`);
841
878
  }
842
879
 
880
+ /** A session's sandbox perf HISTORY — 15-minute rollup buckets
881
+ * (CPU/mem/disk percentiles, load, saturation seconds), newest first.
882
+ * Member-gated like the session itself. Cursor-paginated. */
883
+ getSessionPerfHistory(
884
+ conversationId: string, opts?: { limit?: number; cursor?: string },
885
+ ): Promise<SessionPerfHistoryPage> {
886
+ const q = new URLSearchParams();
887
+ if (opts?.limit !== undefined) q.set("limit", String(opts.limit));
888
+ if (opts?.cursor) q.set("cursor", opts.cursor);
889
+ return this.fetch<SessionPerfHistoryPage>(
890
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/perf-history${q.toString() ? `?${q}` : ""}`);
891
+ }
892
+
893
+ /** Team-level sandbox perf aggregates over a window (default trailing
894
+ * 24h, max 30 days). Aggregate-only — no per-session identifiers. */
895
+ getFactoryPerfSummary(
896
+ factorySlug: string, opts?: { from?: string; to?: string },
897
+ ): Promise<FactoryPerfSummary> {
898
+ const q = new URLSearchParams();
899
+ if (opts?.from) q.set("from", opts.from);
900
+ if (opts?.to) q.set("to", opts.to);
901
+ return this.fetch<FactoryPerfSummary>(
902
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/perf-summary${q.toString() ? `?${q}` : ""}`);
903
+ }
904
+
843
905
  /** One conversation with its latest page of messages. `limit` bounds the
844
906
  * page (newest N) — metadata-only consumers (e.g. resolving the
845
907
  * session's drive branch) pass 1 instead of pulling the full hydrate. */
@@ -944,6 +1006,51 @@ export class AgentComposeClient {
944
1006
  );
945
1007
  }
946
1008
 
1009
+ /** Message ANOTHER session's conversation AS the calling session
1010
+ * (`agentc session message @alias`). Session toolbelt credential ONLY —
1011
+ * the sender resolves from the key. `target` is an @alias (team-scoped)
1012
+ * or a conversation uuid. Unlike a channel post this WAKES the target's
1013
+ * turn machinery; the response's `turn` verdict says whether it started,
1014
+ * queued behind a live turn, or was parked by the agent-to-agent
1015
+ * exchange damper. Unknown alias → 404 with `candidates`. */
1016
+ sendSessionMessage(input: { target: string; text: string }): Promise<SessionDirectMessageSent> {
1017
+ return this.fetch<SessionDirectMessageSent>("/api/v1/session-messages", {
1018
+ method: "POST", body: input,
1019
+ });
1020
+ }
1021
+
1022
+ /** Claim ANOTHER session's drive branch for the calling session
1023
+ * (`agentc branch claim @alias`) — transfers the branch's one-writer
1024
+ * authority: the target's own writes refuse honestly until release, and
1025
+ * the response carries the mount token the CLI uses to FUSE-mount the
1026
+ * branch in-guest. Session toolbelt credential only; same-owner sessions
1027
+ * only (cross-user claims refuse with the gap stated); refuses while the
1028
+ * target is mid-turn (409 `mid_turn`) or already claimed
1029
+ * (409 `already_claimed`). */
1030
+ claimSessionBranch(input: { target: string; reason?: string }): Promise<BranchClaimGranted> {
1031
+ return this.fetch<BranchClaimGranted>("/api/v1/session-branch-claims", {
1032
+ method: "POST", body: { target: input.target, ...(input.reason ? { reason: input.reason } : {}) },
1033
+ });
1034
+ }
1035
+
1036
+ /** Release branch claims HELD by the calling session (`agentc branch
1037
+ * release [@alias]`). `target` narrows to one claim; omitted releases
1038
+ * them all. Idempotent — an empty `released` list means nothing was
1039
+ * held. */
1040
+ releaseSessionBranchClaims(input: { target?: string } = {}): Promise<BranchClaimsReleased> {
1041
+ return this.fetch<BranchClaimsReleased>("/api/v1/session-branch-claims/release", {
1042
+ method: "POST", body: { ...(input.target ? { target: input.target } : {}) },
1043
+ });
1044
+ }
1045
+
1046
+ /** One session's branch-claim state: the live claim ON its branch (null =
1047
+ * writable there) and the claims it HOLDS (`agentc branch status`, the
1048
+ * dashboard's Changes-panel badge). Any viewer with access. */
1049
+ getSessionBranchClaimState(conversationId: string): Promise<BranchClaimState> {
1050
+ return this.fetch<BranchClaimState>(
1051
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/branch-claim`);
1052
+ }
1053
+
947
1054
  // ── Cloud-session developer surface (ADR-0052) ────────────────────────────
948
1055
 
949
1056
  /** Open a dev preview on a cloud session: expose a port a dev server is
@@ -984,6 +1091,28 @@ export class AgentComposeClient {
984
1091
  return body.closed;
985
1092
  }
986
1093
 
1094
+ /** Hold (or extend — the stamp is monotone) a cloud session's
1095
+ * background-work busy lease: while it is live, the between-turns
1096
+ * park/suspend leaves the session's VM running so work the turn left
1097
+ * behind keeps executing. The lease lapses on its own — re-hold to
1098
+ * extend past `minutes` (server-capped). Write-tier. */
1099
+ holdBackgroundWork(conversationId: string, input?: { minutes?: number }): Promise<BackgroundWorkHeld> {
1100
+ return this.fetch<BackgroundWorkHeld>(
1101
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`,
1102
+ { method: "POST", body: { ...(input?.minutes !== undefined ? { minutes: input.minutes } : {}) } },
1103
+ );
1104
+ }
1105
+
1106
+ /** Release the session's background-work lease (the work finished).
1107
+ * Idempotent — `released` is false when no lease was held. */
1108
+ async releaseBackgroundWork(conversationId: string): Promise<boolean> {
1109
+ const body = await this.fetch<{ released: boolean }>(
1110
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`,
1111
+ { method: "DELETE" },
1112
+ );
1113
+ return body.released;
1114
+ }
1115
+
987
1116
  /** Fork a cloud session from HEAD: snapshot the VM + branch the drive + seed
988
1117
  * a new conversation from the transcript so far, booting from both. Returns
989
1118
  * the child conversation id to switch to. Write-tier; "Branch from here." */
@@ -1006,9 +1135,82 @@ export class AgentComposeClient {
1006
1135
  * writes `main` directly. The list reflects the branch's last flush and
1007
1136
  * is advisory; a merge re-verifies against fresh state. 404 = not found
1008
1137
  * or not a member (uniform). */
1009
- getSessionChanges(conversationId: string): Promise<SessionChangeSet> {
1138
+ getSessionChanges(
1139
+ conversationId: string,
1140
+ opts: { after?: string; limit?: number } = {},
1141
+ ): Promise<SessionChangeSet> {
1142
+ // Graph-plane pagination (additive): `after` = the previous page's
1143
+ // `nextAfter` cursor; `limit` 1..1000. Older servers ignore both.
1144
+ const q = new URLSearchParams();
1145
+ if (opts.after !== undefined) q.set("after", opts.after);
1146
+ if (opts.limit !== undefined) q.set("limit", String(opts.limit));
1147
+ const tail = q.toString();
1010
1148
  return this.fetch<SessionChangeSet>(
1011
- `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes`,
1149
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes${tail ? `?${tail}` : ""}`,
1150
+ );
1151
+ }
1152
+
1153
+ /** One proposed file's RAW BYTES from a session's change set
1154
+ * (`GET /conversations/:id/changes/content`) — the review surface's
1155
+ * per-file read, also the in-sandbox `agentc review file` door.
1156
+ * `side: "base"` serves the PRE-change side (graph plane; the legacy
1157
+ * plane ignores it and always serves the proposed side). 404 = missing
1158
+ * path or no access (uniform, no existence oracle). */
1159
+ async getSessionChangeContent(
1160
+ conversationId: string, path: string, opts: { side?: "base" } = {},
1161
+ ): Promise<Uint8Array> {
1162
+ const q = new URLSearchParams({ path });
1163
+ if (opts.side !== undefined) q.set("side", opts.side);
1164
+ const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
1165
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/content?${q}`,
1166
+ { responseType: "arrayBuffer" },
1167
+ );
1168
+ return new Uint8Array(buf);
1169
+ }
1170
+
1171
+ /** Publish a diff review's WHOLE document — prose notes plus structured
1172
+ * suggestions, FULL REPLACE on every call (`POST
1173
+ * /conversations/:id/review-notes`; `agentc review publish`). Callable
1174
+ * ONLY with the bound review session's own toolbelt credential — any
1175
+ * other key (another session, a human cookie, a plain API key) refuses
1176
+ * 403. `conversationId` is the REVIEWED session's conversation (in a
1177
+ * review sandbox: `AGENT_COMPOSE_REVIEW_OF_CONVERSATION_ID`).
1178
+ *
1179
+ * Failures: 403 `session_credential_required` | `not_review_session` |
1180
+ * `review_superseded` (a newer review replaced this one); 400
1181
+ * `suggestion_path_not_in_change_set` (naming the offenders); 409
1182
+ * `review_disabled`. */
1183
+ publishReviewNotes(
1184
+ conversationId: string,
1185
+ input: { notes: string; suggestions?: ReviewSuggestionInput[] },
1186
+ ): Promise<ReviewNotesPublished> {
1187
+ return this.fetch<ReviewNotesPublished>(
1188
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/review-notes`,
1189
+ {
1190
+ method: "POST",
1191
+ body: {
1192
+ notes: input.notes,
1193
+ ...(input.suggestions !== undefined ? { suggestions: input.suggestions } : {}),
1194
+ },
1195
+ },
1196
+ );
1197
+ }
1198
+
1199
+ /** Mint ONE fresh short-lived credential for the review git remote
1200
+ * (`POST /conversations/:id/review-git-credential`) — the server half
1201
+ * of the review sandbox's git credential helper (`agentc review
1202
+ * git-credential`), which calls this per git operation so the remote's
1203
+ * credential can never expire mid-review. Callable only with a review
1204
+ * session's own toolbelt credential; `conversationId` is the REVIEWED
1205
+ * session's conversation (`AGENT_COMPOSE_REVIEW_OF_CONVERSATION_ID`).
1206
+ *
1207
+ * Failures: 403 `session_credential_required` | `not_review_session`;
1208
+ * 409 `review_disabled` | `git_remote_unavailable` (legacy plane — read
1209
+ * the proposal through `getSessionChanges` instead). */
1210
+ getReviewGitCredential(conversationId: string): Promise<ReviewGitCredential> {
1211
+ return this.fetch<ReviewGitCredential>(
1212
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/review-git-credential`,
1213
+ { method: "POST" },
1012
1214
  );
1013
1215
  }
1014
1216
 
@@ -1017,6 +1219,11 @@ export class AgentComposeClient {
1017
1219
  * `main`. Write-tier + human caller. Pass `opts.branch` (the branch you
1018
1220
  * reviewed) to fail 409 `branch_changed` if it moved since.
1019
1221
  *
1222
+ * On a merge-GATED session (merge-gate spec) a non-approver's call does
1223
+ * NOT merge: it answers 202 `SessionMergeGated` — the ask froze into (or
1224
+ * converged on) a kind='merge' approval routed to the session's
1225
+ * approvers. Discriminate on `object`.
1226
+ *
1020
1227
  * Failures: 403 `role_read_only` (read-only member) or a plain 403 for
1021
1228
  * session toolbelt keys (agents cannot self-approve); 409 `turn_active`
1022
1229
  * (a turn is running — retry when idle) | `branch_changed`; 400
@@ -1026,8 +1233,8 @@ export class AgentComposeClient {
1026
1233
  mergeSessionChanges(
1027
1234
  conversationId: string,
1028
1235
  opts: { branch?: string } = {},
1029
- ): Promise<SessionMergeReport> {
1030
- return this.fetch<SessionMergeReport>(
1236
+ ): Promise<SessionMergeReport | SessionMergeGated> {
1237
+ return this.fetch<SessionMergeReport | SessionMergeGated>(
1031
1238
  `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/merge`,
1032
1239
  { method: "POST", body: opts.branch ? { branch: opts.branch } : {} },
1033
1240
  );
@@ -1048,6 +1255,59 @@ export class AgentComposeClient {
1048
1255
  );
1049
1256
  }
1050
1257
 
1258
+ /** The conflict PREFLIGHT: which of the session's changed files would
1259
+ * conflict with main if merged right now — the real merge's own per-file
1260
+ * classification, report-only (no writes, no locks). Read-tier.
1261
+ * Discriminate on `state`: `pending` means a cold compute is still
1262
+ * running — poll again to collect it; `unavailable` = no cheap anchors
1263
+ * (legacy plane / unbranched) — degrade honestly. */
1264
+ getSessionChangePreflight(conversationId: string): Promise<SessionChangePreflight> {
1265
+ return this.fetch<SessionChangePreflight>(
1266
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/preflight`,
1267
+ );
1268
+ }
1269
+
1270
+ /** Rebase the session's branch ON main — fold main's current state
1271
+ * into the session's workspace (the reverse merge; main is never
1272
+ * touched). Conflicting files KEEP the session's version: each is
1273
+ * recorded as a conflict row and the session is told in-conversation to
1274
+ * reconcile. Write-tier — a session's own toolbelt key may rebase its
1275
+ * OWN branch (unlike merge/discard, which need a human). Pass
1276
+ * `opts.branch` to fail 409 `branch_changed` if it rotated since review.
1277
+ *
1278
+ * Failures: 409 `plane_unsupported` (legacy drive plane) |
1279
+ * `review_session_scratch` | `branch_moved` (the branch advanced
1280
+ * mid-rebase — retry) | `turn_active` | `decision_in_progress` |
1281
+ * `freshen_failed` / `merge_retry` (mount mid-handoff — retry); 400
1282
+ * `no_branch`; 502 `rebase_failed` (nothing changed — retry-safe). */
1283
+ rebaseSessionChanges(
1284
+ conversationId: string,
1285
+ opts: { branch?: string } = {},
1286
+ ): Promise<SessionRebaseReport> {
1287
+ return this.fetch<SessionRebaseReport>(
1288
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/rebase`,
1289
+ { method: "POST", body: opts.branch ? { branch: opts.branch } : {} },
1290
+ );
1291
+ }
1292
+
1293
+ /** Arm/disarm the OPT-IN main-advance auto-rebase reflex for a session
1294
+ * (default OFF): when armed and the drive's main moves, the platform
1295
+ * preflights and — only on a provably clean fold (zero conflicts) —
1296
+ * rebases the session's branch from main headlessly, posting a quiet
1297
+ * receipt. Trade-off (why default OFF): the agent's view of files it
1298
+ * did not touch can change mid-task. Human, write-tier act.
1299
+ *
1300
+ * Failures: 409 `review_session_scratch` | `plane_unsupported`; 403
1301
+ * (needs a user-bound human caller with write access). */
1302
+ setSessionAutoRebase(
1303
+ conversationId: string, enabled: boolean,
1304
+ ): Promise<SessionAutoRebaseState> {
1305
+ return this.fetch<SessionAutoRebaseState>(
1306
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/autorebase`,
1307
+ { method: "POST", body: { enabled } },
1308
+ );
1309
+ }
1310
+
1051
1311
  // ── Conversation membership (ADR-0045) ────────────────────────────────────
1052
1312
  // Channel rosters carry roles (`owner > write > read`). Inviting is a
1053
1313
  // `write` action; removing and re-roling are `owner` (or team admin)
@@ -1139,10 +1399,15 @@ export class AgentComposeClient {
1139
1399
  * scoped, not surface-scoped: any member can cancel, whichever surface
1140
1400
  * started the turn. The canceled turn closes with a terminal error part
1141
1401
  * (it is never re-run); a message sent after it stays queued and is
1142
- * answered next. `canceled: false` = nothing was in flight. Requires
1402
+ * answered next. `canceled: false` = nothing was in flight. `owed`
1403
+ * reports the queued-backlog kick: 'dispatched' = a turn answering the
1404
+ * queue was started by this call; 'held' = a live release loop owns the
1405
+ * backlog; 'none' = nothing queued (absent on older servers). Requires
1143
1406
  * the `invoke` scope. */
1144
- cancelConversationTurn(conversationId: string): Promise<{ canceled: boolean }> {
1145
- return this.fetch<{ canceled: boolean }>(
1407
+ cancelConversationTurn(
1408
+ conversationId: string,
1409
+ ): Promise<{ canceled: boolean; owed?: "dispatched" | "held" | "none" }> {
1410
+ return this.fetch<{ canceled: boolean; owed?: "dispatched" | "held" | "none" }>(
1146
1411
  `/api/v1/conversations/${encodeURIComponent(conversationId)}/cancel-turn`,
1147
1412
  { method: "POST", body: {} },
1148
1413
  );
@@ -1205,6 +1470,33 @@ export class AgentComposeClient {
1205
1470
  );
1206
1471
  }
1207
1472
 
1473
+ /** List a factory drive's files — a flat recursive listing, cursor-
1474
+ * paginated by path. The live index by default; pass `at` (a retained
1475
+ * 64-hex commit) to browse the MAIN drive as of that head — read-only
1476
+ * time travel, with `branch` + `at` pinning a graph branch instead. A
1477
+ * garbage-collected/unknown head throws 410 `at_unavailable`. */
1478
+ async listFactoryFiles(opts?: ListFactoryFilesOptions): Promise<FactoryFileListPage> {
1479
+ const factorySlug = opts?.factorySlug
1480
+ ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
1481
+ ?? DEFAULT_FACTORY;
1482
+ const q = new URLSearchParams();
1483
+ if (opts?.prefix) q.set("prefix", opts.prefix);
1484
+ if (opts?.cursor) q.set("cursor", opts.cursor);
1485
+ if (opts?.limit != null) q.set("limit", String(opts.limit));
1486
+ if (opts?.branch !== undefined) q.set("branch", opts.branch);
1487
+ if (opts?.at !== undefined) q.set("at", opts.at);
1488
+ const qs = q.toString();
1489
+ const wire = await this.fetch<FactoryFileListWire>(
1490
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/files${qs ? `?${qs}` : ""}`,
1491
+ );
1492
+ return {
1493
+ data: wire.data,
1494
+ hasMore: wire.has_more,
1495
+ nextCursor: wire.next_cursor,
1496
+ at: wire.at ? { head: wire.at.head, committedAt: wire.at.committed_at } : null,
1497
+ };
1498
+ }
1499
+
1208
1500
  /** List the human members of your team — the people you (or an agent) can
1209
1501
  * @-flag with `createMentions`. Each row's `userId` is what
1210
1502
  * `mentionedUserIds` expects. */
@@ -1608,17 +1900,18 @@ export class AgentComposeClient {
1608
1900
  return body.links;
1609
1901
  }
1610
1902
 
1611
- /** Link a drive directory to a GitHub repo + tracked branch (`manage`
1612
- * scope). Requires the drive to be graph-authoritative — 409 names the
1613
- * promotion prerequisite otherwise. */
1903
+ /** Link a drive directory to a GitHub repo's tracked BRANCHES (`manage`
1904
+ * scope) — one link row per branch, created in one action. Requires the
1905
+ * drive to be graph-authoritative — 409 names the promotion
1906
+ * prerequisite otherwise. */
1614
1907
  async createRepoLink(
1615
1908
  input: CreateDriveRepoLinkInput, opts?: { factorySlug?: string },
1616
- ): Promise<DriveRepoLink> {
1909
+ ): Promise<DriveRepoLink[]> {
1617
1910
  const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
1618
- const body = await this.fetch<{ link: DriveRepoLink }>(
1911
+ const body = await this.fetch<{ links: DriveRepoLink[] }>(
1619
1912
  `/api/v1/factories/${encodeURIComponent(slug)}/repo-links`,
1620
1913
  { method: "POST", body: input });
1621
- return body.link;
1914
+ return body.links;
1622
1915
  }
1623
1916
 
1624
1917
  /** Unlink (§6.3: the prefix's files and history stay on the drive). */
@@ -1655,6 +1948,58 @@ export class AgentComposeClient {
1655
1948
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
1656
1949
  }
1657
1950
 
1951
+ // ── Session secrets ─────────────────────────────────────────────────────────
1952
+ // Per-session env for a cloud session's sandbox, keyed by the session's
1953
+ // CONVERSATION id. Values are write-only (set/rotate/delete — never read
1954
+ // back); each secret has an OWNER (the user who set it) and only the owner
1955
+ // or a team admin may replace/remove it. `source: "factory"` entries attach
1956
+ // a factory secret BY NAME instead of carrying a value. Changes apply from
1957
+ // the session's next turn.
1958
+
1959
+ /** List a session's secrets (names + metadata only — values are never returned). */
1960
+ async listSessionSecrets(conversationId: string): Promise<SessionSecretEntry[]> {
1961
+ const body = await this.fetch<{ secrets: SessionSecretEntry[] }>(
1962
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets`);
1963
+ return body.secrets;
1964
+ }
1965
+
1966
+ /** Set session secrets (bulk-friendly): value entries and/or `source:
1967
+ * "factory"` attachments-by-name. Returns the keys that were set. */
1968
+ async setSessionSecrets(conversationId: string, secrets: SessionSecretInput[]): Promise<string[]> {
1969
+ const body = await this.fetch<{ set: string[] }>(
1970
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets`,
1971
+ { method: "POST", body: { secrets } });
1972
+ return body.set;
1973
+ }
1974
+
1975
+ /** Delete one session secret (owner or team admin). */
1976
+ deleteSessionSecret(conversationId: string, key: string): Promise<void> {
1977
+ return this.fetch(
1978
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets/${encodeURIComponent(key)}`,
1979
+ { method: "DELETE" });
1980
+ }
1981
+
1982
+ /** Mint a VAULT LINK for this session — a short-lived, single-use page
1983
+ * where a session writer types the named credentials straight into the
1984
+ * session's secret store (they never transit the chat). Posts a card
1985
+ * into the session conversation; callable with the session's own
1986
+ * toolbelt credential (the in-sandbox agent's door) or by a session
1987
+ * writer. */
1988
+ requestSessionSecrets(
1989
+ conversationId: string, keys: string[], opts?: { reason?: string },
1990
+ ): Promise<SessionSecretRequestCreated> {
1991
+ return this.fetch(
1992
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secret-requests`,
1993
+ { method: "POST", body: { keys, ...(opts?.reason ? { reason: opts.reason } : {}) } });
1994
+ }
1995
+
1996
+ /** Poll a vault link's status (`pending` → `fulfilled`; `expired` when the
1997
+ * clock ran out). The in-sandbox `agentc secrets request --wait` loop. */
1998
+ getSessionSecretRequest(conversationId: string, requestId: string): Promise<SessionSecretRequestStatus> {
1999
+ return this.fetch(
2000
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secret-requests/${encodeURIComponent(requestId)}`);
2001
+ }
2002
+
1658
2003
  // ── API keys ───────────────────────────────────────────────────────────────
1659
2004
  // Both endpoints require an admin-scoped key as the bearer token.
1660
2005
 
@@ -1668,10 +2013,17 @@ export class AgentComposeClient {
1668
2013
  }
1669
2014
 
1670
2015
  /** List API keys on the caller's team (metadata only — plaintext keys are
1671
- * never returned). */
1672
- async listApiKeys(): Promise<ApiKey[]> {
1673
- const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean }>("/api-keys");
1674
- return body.data;
2016
+ * never returned). Bounded + keyset-paginated: pass `cursor` from the
2017
+ * previous page's `nextCursor` to walk forward. */
2018
+ async listApiKeys(opts?: ListApiKeysOptions): Promise<ApiKeyPage> {
2019
+ const qs = new URLSearchParams();
2020
+ if (opts?.limit !== undefined) qs.set("limit", String(opts.limit));
2021
+ if (opts?.status !== undefined) qs.set("status", opts.status);
2022
+ if (opts?.cursor !== undefined) qs.set("cursor", opts.cursor);
2023
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
2024
+ const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean; next_cursor: string | null }>(
2025
+ `/api-keys${suffix}`);
2026
+ return { data: body.data, hasMore: body.has_more, nextCursor: body.next_cursor ?? null };
1675
2027
  }
1676
2028
 
1677
2029
  // ── Usage ──────────────────────────────────────────────────────────────────