@cat-factory/server 0.253.0 → 0.255.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 (58) hide show
  1. package/dist/agents/containerAgentBody.d.ts.map +1 -1
  2. package/dist/agents/containerAgentBody.js +30 -0
  3. package/dist/agents/containerAgentBody.js.map +1 -1
  4. package/dist/agents/harnessContract.d.ts +2 -0
  5. package/dist/agents/harnessContract.d.ts.map +1 -1
  6. package/dist/agents/harnessContract.js +3 -1
  7. package/dist/agents/harnessContract.js.map +1 -1
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +10 -2
  10. package/dist/app.js.map +1 -1
  11. package/dist/modules/artifacts/ArtifactController.d.ts.map +1 -1
  12. package/dist/modules/artifacts/ArtifactController.js +48 -3
  13. package/dist/modules/artifacts/ArtifactController.js.map +1 -1
  14. package/dist/modules/artifacts/HarnessArtifactController.d.ts.map +1 -1
  15. package/dist/modules/artifacts/HarnessArtifactController.js +99 -55
  16. package/dist/modules/artifacts/HarnessArtifactController.js.map +1 -1
  17. package/dist/modules/artifacts/artifactSetCap.d.ts +31 -0
  18. package/dist/modules/artifacts/artifactSetCap.d.ts.map +1 -0
  19. package/dist/modules/artifacts/artifactSetCap.js +47 -0
  20. package/dist/modules/artifacts/artifactSetCap.js.map +1 -0
  21. package/dist/modules/publicApi/PublicApiController.d.ts.map +1 -1
  22. package/dist/modules/publicApi/PublicApiController.js +16 -33
  23. package/dist/modules/publicApi/PublicApiController.js.map +1 -1
  24. package/dist/modules/publicApi/PublicBoardController.d.ts +4 -0
  25. package/dist/modules/publicApi/PublicBoardController.d.ts.map +1 -0
  26. package/dist/modules/publicApi/PublicBoardController.js +241 -0
  27. package/dist/modules/publicApi/PublicBoardController.js.map +1 -0
  28. package/dist/modules/publicApi/PublicEvidenceController.d.ts.map +1 -1
  29. package/dist/modules/publicApi/PublicEvidenceController.js +43 -5
  30. package/dist/modules/publicApi/PublicEvidenceController.js.map +1 -1
  31. package/dist/modules/publicApi/boardProjection.d.ts +6 -0
  32. package/dist/modules/publicApi/boardProjection.d.ts.map +1 -0
  33. package/dist/modules/publicApi/boardProjection.js +39 -0
  34. package/dist/modules/publicApi/boardProjection.js.map +1 -0
  35. package/dist/modules/publicApi/decisions/projection.d.ts +16 -7
  36. package/dist/modules/publicApi/decisions/projection.d.ts.map +1 -1
  37. package/dist/modules/publicApi/decisions/projection.js +23 -15
  38. package/dist/modules/publicApi/decisions/projection.js.map +1 -1
  39. package/dist/modules/publicApi/documentAttachment.d.ts +10 -2
  40. package/dist/modules/publicApi/documentAttachment.d.ts.map +1 -1
  41. package/dist/modules/publicApi/documentAttachment.js +1 -1
  42. package/dist/modules/publicApi/documentAttachment.js.map +1 -1
  43. package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
  44. package/dist/modules/publicApi/openapiDocument.generated.d.ts.map +1 -1
  45. package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
  46. package/dist/modules/publicApi/openapiDocument.generated.js.map +1 -1
  47. package/dist/modules/publicApi/publicApiAdmission.d.ts +56 -47
  48. package/dist/modules/publicApi/publicApiAdmission.d.ts.map +1 -1
  49. package/dist/modules/publicApi/publicApiAdmission.js +51 -51
  50. package/dist/modules/publicApi/publicApiAdmission.js.map +1 -1
  51. package/dist/modules/publicApi/publicApiStream.d.ts +29 -1
  52. package/dist/modules/publicApi/publicApiStream.d.ts.map +1 -1
  53. package/dist/modules/publicApi/publicApiStream.js +45 -0
  54. package/dist/modules/publicApi/publicApiStream.js.map +1 -1
  55. package/dist/persistence/rpc-allowlist.d.ts.map +1 -1
  56. package/dist/persistence/rpc-allowlist.js +1 -0
  57. package/dist/persistence/rpc-allowlist.js.map +1 -1
  58. package/package.json +8 -8
@@ -3,8 +3,9 @@ import { bodyLimit } from 'hono/body-limit';
3
3
  import { bearerToken } from '../../auth/middleware.js';
4
4
  import { ContainerSessionService } from '../../containers/ContainerSessionService.js';
5
5
  import { logger } from '../../observability/logger.js';
6
- import { MAX_REQUEST_BYTES, MAX_UPLOAD_BYTES, exceedsRequestSizeLimit, normalizeImageContentType, } from './imageArtifacts.js';
7
- import { UnavailableError, UnauthorizedError } from '@cat-factory/kernel';
6
+ import { MAX_REQUEST_BYTES, MAX_UPLOAD_BYTES, blobResponseHeaders, exceedsRequestSizeLimit, normalizeImageContentType, } from './imageArtifacts.js';
7
+ import { NotFoundError, UnavailableError, UnauthorizedError } from '@cat-factory/kernel';
8
+ import { reclaimArtifactOverflow, reserveArtifactSlot } from './artifactSetCap.js';
8
9
  /**
9
10
  * Cap on how many screenshots a single run may upload. A `tester-ui` run captures one shot per
10
11
  * distinct view, so a couple of dozen is generous; the ceiling stops a buggy or compromised
@@ -12,6 +13,24 @@ import { UnavailableError, UnauthorizedError } from '@cat-factory/kernel';
12
13
  * count is read back from the store per ingest (cheap, indexed by execution).
13
14
  */
14
15
  const MAX_SCREENSHOTS_PER_RUN = 100;
16
+ /** One run's capture set, as {@link reserveArtifactSlot} / {@link reclaimArtifactOverflow} see it. */
17
+ function runCap(store, workspaceId, executionId) {
18
+ return {
19
+ limit: MAX_SCREENSHOTS_PER_RUN,
20
+ count: () => store.countByExecution(workspaceId, executionId),
21
+ list: () => store.listByExecution(workspaceId, executionId),
22
+ remove: (id) => store.delete(workspaceId, id),
23
+ };
24
+ }
25
+ /** The one refusal both cap checks answer with, so the pre-check and the reconcile cannot differ. */
26
+ function refuseFullRun(c, executionId) {
27
+ logger.warn('artifact ingest: per-run screenshot limit reached', {
28
+ scope: 'artifactIngest',
29
+ executionId,
30
+ limit: MAX_SCREENSHOTS_PER_RUN,
31
+ });
32
+ return c.json({ error: { code: 'too_many', message: 'Per-run screenshot limit reached' } }, 429);
33
+ }
15
34
  /**
16
35
  * Resolve the stored content type for an uploaded screenshot. Screenshots are always PNGs, so a
17
36
  * typeless upload defaults to PNG; a declared type is gated through the shared image allow-list.
@@ -25,6 +44,41 @@ function resolveScreenshotContentType(declaredType) {
25
44
  }
26
45
  return normalizeImageContentType(trimmed);
27
46
  }
47
+ /**
48
+ * Verify the container session token on a harness request and resolve the store its run may use.
49
+ *
50
+ * One helper for both directions of the seam (the tester's screenshot ingest and the reference
51
+ * download beside it), so the two can never end up disagreeing about what authenticates a
52
+ * container: the SAME short-lived, workspace- and execution-pinned token the agent already holds
53
+ * for the LLM proxy, and a store resolved from the token's workspace rather than from anything the
54
+ * request says.
55
+ */
56
+ async function requireHarnessSession(c, scope) {
57
+ const container = c.get('container');
58
+ const resolveStore = container.resolveBinaryArtifactStore;
59
+ if (!resolveStore) {
60
+ throw new UnavailableError('Artifact storage not configured');
61
+ }
62
+ const secret = container.config.auth.sessionSecret;
63
+ if (!secret) {
64
+ logger.error('harness artifacts: session secret not configured', { scope });
65
+ throw new UnavailableError('Artifact ingest not configured');
66
+ }
67
+ const sessions = new ContainerSessionService({ secret });
68
+ const session = await sessions.verify(bearerToken(c));
69
+ if (!session) {
70
+ logger.warn('harness artifacts: invalid or expired session token', { scope });
71
+ throw new UnauthorizedError('Invalid or expired token');
72
+ }
73
+ // The store is the run's ACCOUNT's configured backend, resolved from the token's workspace
74
+ // (never the request), so a container only ever reaches its own account's storage. Null means the
75
+ // account configured no storage.
76
+ const store = await resolveStore(session.workspaceId);
77
+ if (!store) {
78
+ throw new UnavailableError('Artifact storage not configured');
79
+ }
80
+ return { session, store };
81
+ }
28
82
  /**
29
83
  * The in-container screenshot ingest endpoint for the UI tester (`tester-ui`). It lives on
30
84
  * the harness path (mounted at `/`, reachable at `${proxyBaseUrl}/artifacts/ingest`) and is
@@ -44,49 +98,21 @@ export function harnessArtifactController() {
44
98
  maxSize: MAX_REQUEST_BYTES,
45
99
  onError: (c) => c.json({ error: { code: 'too_large', message: 'Artifact exceeds size limit' } }, 413),
46
100
  }), async (c) => {
47
- const container = c.get('container');
48
- const resolveStore = container.resolveBinaryArtifactStore;
49
- if (!resolveStore) {
50
- throw new UnavailableError('Artifact storage not configured');
51
- }
52
- const secret = container.config.auth.sessionSecret;
53
- if (!secret) {
54
- logger.error('artifact ingest: session secret not configured', { scope: 'artifactIngest' });
55
- throw new UnavailableError('Artifact ingest not configured');
56
- }
57
- const sessions = new ContainerSessionService({ secret });
58
- const session = await sessions.verify(bearerToken(c));
59
- if (!session) {
60
- logger.warn('artifact ingest: invalid or expired session token', {
61
- scope: 'artifactIngest',
62
- });
63
- throw new UnauthorizedError('Invalid or expired token');
64
- }
65
- // The store is the run's ACCOUNT's configured backend, resolved from the token's
66
- // workspace (never the request body) — so a container can only write to its own
67
- // account's storage. Null ⇒ the account configured no storage.
68
- const store = await resolveStore(session.workspaceId);
69
- if (!store) {
70
- throw new UnavailableError('Artifact storage not configured');
71
- }
101
+ const { session, store } = await requireHarnessSession(c, 'artifactIngest');
72
102
  // Refuse a grossly oversized body from Content-Length before it is buffered into memory; the
73
103
  // exact per-file ceiling is still enforced after parsing below.
74
104
  if (exceedsRequestSizeLimit(c.req.header('content-length'))) {
75
105
  return c.json({ error: { code: 'too_large', message: 'Artifact exceeds size limit' } }, 413);
76
106
  }
77
107
  // Per-run upload ceiling (fast-path): a runaway/compromised container can't fill the store
78
- // with unbounded screenshots scoped to its run. This pre-check rejects the steady-state case
108
+ // with unbounded screenshots scoped to its run. The shared cap rejects the steady-state case
79
109
  // cheaply via an indexed COUNT (no row materialise); concurrent ingests that race past it are
80
110
  // caught by the post-insert reconcile below, so the effective ceiling holds even without a
81
111
  // DB-level atomic counter.
82
- const existingCount = await store.countByExecution(session.workspaceId, session.executionId);
83
- if (existingCount >= MAX_SCREENSHOTS_PER_RUN) {
84
- logger.warn('artifact ingest: per-run screenshot limit reached', {
85
- scope: 'artifactIngest',
86
- executionId: session.executionId,
87
- count: existingCount,
88
- });
89
- return c.json({ error: { code: 'too_many', message: 'Per-run screenshot limit reached' } }, 429);
112
+ const cap = runCap(store, session.workspaceId, session.executionId);
113
+ const priorCount = await reserveArtifactSlot(cap);
114
+ if (priorCount === null) {
115
+ return refuseFullRun(c, session.executionId);
90
116
  }
91
117
  let form;
92
118
  try {
@@ -130,28 +156,46 @@ export function harnessArtifactController() {
130
156
  },
131
157
  blob: bytes,
132
158
  });
133
- // Reconcile the cap against concurrent inserts: the pre-check is check-then-act, so a burst
134
- // of parallel ingests can each pass it before any row lands. We only need to run this
135
- // (which materialises the run's rows to find the overflow tail) when the insert COULD have
136
- // crossed the cap i.e. the pre-check count was already at the edge. Steady-state uploads
137
- // far below the cap skip it entirely, so the common path is one COUNT + one insert.
138
- if (existingCount + 1 >= MAX_SCREENSHOTS_PER_RUN) {
139
- // listByExecution is oldest-first, so anything at index >= the cap is overflow; if THIS
140
- // record is in that tail, roll it back (delete its row + bytes) and reject. The oldest
141
- // `MAX_SCREENSHOTS_PER_RUN` always survive, so the store is bounded to exactly the cap per
142
- // run without dropping legitimate earlier shots.
143
- const after = await store.listByExecution(session.workspaceId, session.executionId);
144
- if (after.length > MAX_SCREENSHOTS_PER_RUN) {
145
- const overflow = new Set(after.slice(MAX_SCREENSHOTS_PER_RUN).map((r) => r.id));
146
- if (overflow.has(record.id)) {
147
- await store.delete(session.workspaceId, record.id);
148
- logger.warn('artifact ingest: per-run screenshot limit reached (post-insert reconcile)', { scope: 'artifactIngest', executionId: session.executionId, count: after.length });
149
- return c.json({ error: { code: 'too_many', message: 'Per-run screenshot limit reached' } }, 429);
150
- }
151
- }
159
+ // Check-then-act, so a burst of parallel ingests can each pass the pre-check before any row
160
+ // lands; the reconcile rolls THIS record back when it is the one that overflowed, leaving the
161
+ // oldest `MAX_SCREENSHOTS_PER_RUN` untouched.
162
+ if (await reclaimArtifactOverflow(cap, priorCount, record.id)) {
163
+ return refuseFullRun(c, session.executionId);
152
164
  }
153
165
  return c.json({ artifactId: record.id }, 201);
154
166
  });
167
+ // Stream one REFERENCE design image back into a container: the other direction of the ingest
168
+ // seam above, and what turns the manifest in a capturing job's body into the files under
169
+ // `.cat-context/reference-screenshots/`. Same auth (the run's own container session token),
170
+ // same store resolution (from the token's workspace, never the request).
171
+ //
172
+ // Two things bound what this can serve. It is scoped to the token's WORKSPACE, so a container
173
+ // can never read another board's designs; and it serves only `kind:'reference'`, so this route
174
+ // cannot become a way for one run to read another run's captured SCREENSHOTS, which is the
175
+ // asymmetry that matters, since references are design material the run was handed on purpose
176
+ // while a screenshot is another run's output. Anything outside that is a 404 rather than a 403:
177
+ // a container has no business learning which artifact ids exist.
178
+ app.get('/v1/artifacts/reference/:id', async (c) => {
179
+ const { session, store } = await requireHarnessSession(c, 'artifactReference');
180
+ const id = c.req.param('id');
181
+ const got = await store.getBlobWithMetadata(session.workspaceId, id);
182
+ // Same two REASONS the public blob route separates, for the same reason: "not yours (or not a
183
+ // reference)" is something to stop asking for, where a metadata row that outlived its bytes is
184
+ // a storage fault. The harness reports either as a reference it could not fetch.
185
+ if (!got || got.record.kind !== 'reference') {
186
+ throw new NotFoundError('Artifact', id, { reason: 'artifact_not_found' });
187
+ }
188
+ if (!got.bytes) {
189
+ throw new NotFoundError('Artifact', id, { reason: 'artifact_blob_missing' });
190
+ }
191
+ // Same headers as the workspace-scoped serve path: the content type is clamped to the image
192
+ // allow-list and `nosniff` is sent, so bytes stored before a tightening can never be served
193
+ // as active content.
194
+ return new Response(got.bytes, {
195
+ status: 200,
196
+ headers: blobResponseHeaders(got.record.contentType),
197
+ });
198
+ });
155
199
  return app;
156
200
  }
157
201
  //# sourceMappingURL=HarnessArtifactController.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"HarnessArtifactController.js","sourceRoot":"","sources":["../../../src/modules/artifacts/HarnessArtifactController.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"HarnessArtifactController.js","sourceRoot":"","sources":["../../../src/modules/artifacts/HarnessArtifactController.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /** One artifact set, as the cap needs to see it. */
2
+ export interface ArtifactSetCap {
3
+ /** Maximum rows the set may hold. */
4
+ limit: number;
5
+ /** Indexed COUNT of the set: no rows materialised. */
6
+ count(): Promise<number>;
7
+ /** The set OLDEST-FIRST. Materialised only when an insert could have crossed the cap. */
8
+ list(): Promise<{
9
+ id: string;
10
+ }[]>;
11
+ /** Drop one row and its bytes. */
12
+ remove(id: string): Promise<void>;
13
+ }
14
+ /**
15
+ * The set's size before this insert when there is room for one more, or null when it is full.
16
+ *
17
+ * The returned count is what {@link reclaimArtifactOverflow} needs afterwards, so the common path
18
+ * costs exactly one COUNT: a caller far below the cap never materialises a row on either side.
19
+ */
20
+ export declare function reserveArtifactSlot(cap: ArtifactSetCap): Promise<number | null>;
21
+ /**
22
+ * Reconcile the cap after an insert: true when `recordId` landed in the overflow tail and was
23
+ * rolled back, so the caller must refuse.
24
+ *
25
+ * Only runs the materialising read when the insert COULD have crossed the cap — that is, when the
26
+ * pre-check already sat at the edge. The set is oldest-first, so everything at or past `limit` is
27
+ * overflow, and only a record of the CALLER'S OWN is rolled back: a concurrent uploader's row is
28
+ * that request's to reject, and dropping it here would refuse one caller by deleting another's.
29
+ */
30
+ export declare function reclaimArtifactOverflow(cap: ArtifactSetCap, priorCount: number, recordId: string): Promise<boolean>;
31
+ //# sourceMappingURL=artifactSetCap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifactSetCap.d.ts","sourceRoot":"","sources":["../../../src/modules/artifacts/artifactSetCap.ts"],"names":[],"mappings":"AAgBA,oDAAoD;AACpD,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,yFAAyF;IACzF,IAAI,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAA;IACjC,kCAAkC;IAClC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGrF;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAQlB"}
@@ -0,0 +1,47 @@
1
+ // The standing row-count bound on an artifact SET (a run's captures, a block's uploaded reference
2
+ // designs), shared by the two upload endpoints so the two cannot drift into different bounds — or
3
+ // into one having none.
4
+ //
5
+ // A bound on the byte size of each upload is not a bound on the set: a caller within every
6
+ // per-file ceiling can still add rows without end, and both sets are read UNPAGED. The run's
7
+ // captures are folded together with the block's uploads by `GET /api/v1/runs/{runId}/artifacts`,
8
+ // whose contract rests on both halves being bounded by construction — so a set with a byte cap and
9
+ // no row cap is the half that quietly withdraws that guarantee.
10
+ //
11
+ // Enforcement is check-then-act plus a post-insert reconcile, because there is no atomic counter
12
+ // under either runtime: the cheap indexed COUNT rejects the steady-state case before anything is
13
+ // buffered, and the reconcile catches the burst that raced past it. Rolling back the OVERFLOW TAIL
14
+ // (never the oldest rows) is what makes the cap safe for a human-facing upload: an uploader is
15
+ // told their upload was refused, rather than silently losing a design they added last week.
16
+ /**
17
+ * The set's size before this insert when there is room for one more, or null when it is full.
18
+ *
19
+ * The returned count is what {@link reclaimArtifactOverflow} needs afterwards, so the common path
20
+ * costs exactly one COUNT: a caller far below the cap never materialises a row on either side.
21
+ */
22
+ export async function reserveArtifactSlot(cap) {
23
+ const existing = await cap.count();
24
+ return existing >= cap.limit ? null : existing;
25
+ }
26
+ /**
27
+ * Reconcile the cap after an insert: true when `recordId` landed in the overflow tail and was
28
+ * rolled back, so the caller must refuse.
29
+ *
30
+ * Only runs the materialising read when the insert COULD have crossed the cap — that is, when the
31
+ * pre-check already sat at the edge. The set is oldest-first, so everything at or past `limit` is
32
+ * overflow, and only a record of the CALLER'S OWN is rolled back: a concurrent uploader's row is
33
+ * that request's to reject, and dropping it here would refuse one caller by deleting another's.
34
+ */
35
+ export async function reclaimArtifactOverflow(cap, priorCount, recordId) {
36
+ if (priorCount + 1 < cap.limit)
37
+ return false;
38
+ const after = await cap.list();
39
+ if (after.length <= cap.limit)
40
+ return false;
41
+ const overflow = new Set(after.slice(cap.limit).map((row) => row.id));
42
+ if (!overflow.has(recordId))
43
+ return false;
44
+ await cap.remove(recordId);
45
+ return true;
46
+ }
47
+ //# sourceMappingURL=artifactSetCap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifactSetCap.js","sourceRoot":"","sources":["../../../src/modules/artifacts/artifactSetCap.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,kGAAkG;AAClG,wBAAwB;AACxB,EAAE;AACF,2FAA2F;AAC3F,6FAA6F;AAC7F,iGAAiG;AACjG,mGAAmG;AACnG,gEAAgE;AAChE,EAAE;AACF,iGAAiG;AACjG,iGAAiG;AACjG,mGAAmG;AACnG,+FAA+F;AAC/F,4FAA4F;AAc5F;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAmB;IAC3D,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAA;IAClC,OAAO,QAAQ,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;AAChD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,GAAmB,EACnB,UAAkB,EAClB,QAAgB;IAEhB,IAAI,UAAU,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IAC5C,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;IAC9B,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IAC3C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IACrE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAA;IACzC,MAAM,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC1B,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"PublicApiController.d.ts","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicApiController.ts"],"names":[],"mappings":"AAwCA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQ3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAoS/C,wBAAgB,mBAAmB,IAAI,IAAI,CAAC,MAAM,CAAC,CAWlD"}
1
+ {"version":3,"file":"PublicApiController.d.ts","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicApiController.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQ3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAiR/C,wBAAgB,mBAAmB,IAAI,IAAI,CAAC,MAAM,CAAC,CAWlD"}
@@ -7,10 +7,11 @@ import { streamSSE } from 'hono/streaming';
7
7
  import { personalGateForBlock, personalGateForRun } from '../providers/personalCredentialGate.js';
8
8
  import { HEADLESS_ACTIONABLE_NOTIFICATION_TYPES, notificationActEffect, } from '../notifications/notificationActions.js';
9
9
  import { authorize } from './publicApiAuth.js';
10
+ import { toPublicService, toPublicTask } from './boardProjection.js';
10
11
  import { createTaskWithAttachments, taskCreationDeps } from './taskCreation.js';
11
12
  import { resolveTaskTypeFieldsPatch } from './taskTypeFields.js';
12
- import { publicRunParkSurfaces, isHeadlessInlinePipeline, isInlineOnlyPipeline, parkingRefusalMessage, PUBLIC_JOB_CANCEL_PATH, PUBLIC_TASK_STOP_PATH, } from './publicApiAdmission.js';
13
- import { createParkAnnouncer, isParked } from './publicApiStream.js';
13
+ import { admissionRegistries, publicRunParkSurfaces, isHeadlessInlinePipeline, isInlineOnlyPipeline, parkingRefusalMessage, PUBLIC_JOB_CANCEL_PATH, PUBLIC_TASK_STOP_PATH, } from './publicApiAdmission.js';
14
+ import { createParkAnnouncer, isParked, reduceRunForStream } from './publicApiStream.js';
14
15
  import { viewRunIdentity } from './runIdentityVisibility.js';
15
16
  import { decodeCursor, decodeTimeCursor, encodeCursor, internalStatusesFor, jobSortKey, mapStatus, } from './publicApiPaging.js';
16
17
  // The PUBLIC external API (`/api/v1/*`). Unlike the SPA surface it is NOT behind the user-session
@@ -87,32 +88,6 @@ function toPublicJob(execution, readerIdentity) {
87
88
  error,
88
89
  };
89
90
  }
90
- /** Project a board task block onto the external task resource (no block/board internals). */
91
- function toPublicTask(block, serviceId) {
92
- return {
93
- taskId: block.id,
94
- serviceId,
95
- title: block.title,
96
- description: block.description,
97
- taskType: block.taskType ?? 'feature',
98
- status: block.status,
99
- progress: block.progress,
100
- // The public name is `runId`, one vocabulary with `publicRun.runId` and `/runs/:runId/...`;
101
- // `executionId` is the internal engine name for the same id.
102
- runId: block.executionId,
103
- pullRequestUrl: block.pullRequest?.url ?? null,
104
- };
105
- }
106
- /** Project a service frame block onto the external service resource. */
107
- function toPublicService(frame) {
108
- return {
109
- serviceId: frame.id,
110
- title: frame.title,
111
- description: frame.description,
112
- type: frame.type,
113
- status: frame.status,
114
- };
115
- }
116
91
  /**
117
92
  * Project a task's persisted run + its block onto the RICH external run resource: per-step
118
93
  * state/progress/subtasks, the failure kind+message, and the PR (url + branch). The run's
@@ -141,6 +116,12 @@ function toPublicRun(execution, block, readerIdentity) {
141
116
  total: s.subtasks.total,
142
117
  }
143
118
  : null,
119
+ // The step's deliverable. An EMPTY output is projected as null, matching the way
120
+ // `toPublicJob` reads "produced something": a step that ran and wrote nothing and a step
121
+ // that has not run yet are the same fact to a caller reading for a result, and the step's
122
+ // own `state` is what distinguishes them.
123
+ output: (s.output ?? '') === '' ? null : (s.output ?? null),
124
+ data: s.custom ?? null,
144
125
  })),
145
126
  // Who the run was started for, as pinned at admission; null for a run the app, a schedule or
146
127
  // an identity-less key started, and withheld (flagged, not blanked) from a key that acts for
@@ -162,13 +143,13 @@ function toPublicRun(execution, block, readerIdentity) {
162
143
  * `pipelineId` for `start` — `public` (job-startable via `POST /jobs`) and `headlessStartable` (safe to
163
144
  * run with no interactive user). Archived pipelines are filtered out by the caller.
164
145
  */
165
- function toPublicPipeline(pipeline, registry) {
146
+ function toPublicPipeline(pipeline, registries) {
166
147
  return {
167
148
  pipelineId: pipeline.id,
168
149
  name: pipeline.name,
169
150
  steps: pipeline.agentKinds.filter((_, i) => pipeline.enabled?.[i] !== false),
170
151
  public: pipeline.public === true,
171
- headlessStartable: isHeadlessInlinePipeline(pipeline, registry),
152
+ headlessStartable: isHeadlessInlinePipeline(pipeline, registries),
172
153
  };
173
154
  }
174
155
  /**
@@ -220,7 +201,7 @@ async function rollbackInitiativeRun(c, workspaceId, blockId) {
220
201
  * moment later.
221
202
  */
222
203
  async function unanswerableParkRefusal(container, auth, pipeline, gateInput, cancelPath) {
223
- const surfaces = publicRunParkSurfaces(pipeline, container.agentKindRegistry, {
204
+ const surfaces = publicRunParkSurfaces(pipeline, admissionRegistries(container), {
224
205
  inputGateBlocks: await container.executionService.inputGateWouldBlock(auth.workspaceId, gateInput),
225
206
  });
226
207
  if (surfaces.length === 0 || scopeSatisfies(auth.scope, 'decide'))
@@ -928,7 +909,9 @@ function registerTaskRunStreamRoute(app) {
928
909
  const block = await container.boardService.getServiceTask(auth.workspaceId, taskId);
929
910
  if (!execution || !block)
930
911
  break;
931
- const runView = toPublicRun(execution, block.block, auth.externalIdentity);
912
+ // Reduced for the wire: the frame carries the whole run, so an oversized step deliverable
913
+ // would be re-sent on every change for the rest of the run. See `reduceRunForStream`.
914
+ const runView = reduceRunForStream(toPublicRun(execution, block.block, auth.externalIdentity));
932
915
  const data = JSON.stringify(runView);
933
916
  if (data !== last) {
934
917
  await stream.writeSSE({
@@ -976,7 +959,7 @@ function registerPipelineRoutes(app) {
976
959
  return c.json({
977
960
  pipelines: pipelines
978
961
  .filter((p) => !p.archived)
979
- .map((p) => toPublicPipeline(p, container.agentKindRegistry)),
962
+ .map((p) => toPublicPipeline(p, admissionRegistries(container))),
980
963
  }, 200);
981
964
  });
982
965
  }