@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.
- package/dist/agents/containerAgentBody.d.ts.map +1 -1
- package/dist/agents/containerAgentBody.js +30 -0
- package/dist/agents/containerAgentBody.js.map +1 -1
- package/dist/agents/harnessContract.d.ts +2 -0
- package/dist/agents/harnessContract.d.ts.map +1 -1
- package/dist/agents/harnessContract.js +3 -1
- package/dist/agents/harnessContract.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +10 -2
- package/dist/app.js.map +1 -1
- package/dist/modules/artifacts/ArtifactController.d.ts.map +1 -1
- package/dist/modules/artifacts/ArtifactController.js +48 -3
- package/dist/modules/artifacts/ArtifactController.js.map +1 -1
- package/dist/modules/artifacts/HarnessArtifactController.d.ts.map +1 -1
- package/dist/modules/artifacts/HarnessArtifactController.js +99 -55
- package/dist/modules/artifacts/HarnessArtifactController.js.map +1 -1
- package/dist/modules/artifacts/artifactSetCap.d.ts +31 -0
- package/dist/modules/artifacts/artifactSetCap.d.ts.map +1 -0
- package/dist/modules/artifacts/artifactSetCap.js +47 -0
- package/dist/modules/artifacts/artifactSetCap.js.map +1 -0
- package/dist/modules/publicApi/PublicApiController.d.ts.map +1 -1
- package/dist/modules/publicApi/PublicApiController.js +16 -33
- package/dist/modules/publicApi/PublicApiController.js.map +1 -1
- package/dist/modules/publicApi/PublicBoardController.d.ts +4 -0
- package/dist/modules/publicApi/PublicBoardController.d.ts.map +1 -0
- package/dist/modules/publicApi/PublicBoardController.js +241 -0
- package/dist/modules/publicApi/PublicBoardController.js.map +1 -0
- package/dist/modules/publicApi/PublicEvidenceController.d.ts.map +1 -1
- package/dist/modules/publicApi/PublicEvidenceController.js +43 -5
- package/dist/modules/publicApi/PublicEvidenceController.js.map +1 -1
- package/dist/modules/publicApi/boardProjection.d.ts +6 -0
- package/dist/modules/publicApi/boardProjection.d.ts.map +1 -0
- package/dist/modules/publicApi/boardProjection.js +39 -0
- package/dist/modules/publicApi/boardProjection.js.map +1 -0
- package/dist/modules/publicApi/decisions/projection.d.ts +16 -7
- package/dist/modules/publicApi/decisions/projection.d.ts.map +1 -1
- package/dist/modules/publicApi/decisions/projection.js +23 -15
- package/dist/modules/publicApi/decisions/projection.js.map +1 -1
- package/dist/modules/publicApi/documentAttachment.d.ts +10 -2
- package/dist/modules/publicApi/documentAttachment.d.ts.map +1 -1
- package/dist/modules/publicApi/documentAttachment.js +1 -1
- package/dist/modules/publicApi/documentAttachment.js.map +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.d.ts.map +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.js.map +1 -1
- package/dist/modules/publicApi/publicApiAdmission.d.ts +56 -47
- package/dist/modules/publicApi/publicApiAdmission.d.ts.map +1 -1
- package/dist/modules/publicApi/publicApiAdmission.js +51 -51
- package/dist/modules/publicApi/publicApiAdmission.js.map +1 -1
- package/dist/modules/publicApi/publicApiStream.d.ts +29 -1
- package/dist/modules/publicApi/publicApiStream.d.ts.map +1 -1
- package/dist/modules/publicApi/publicApiStream.js +45 -0
- package/dist/modules/publicApi/publicApiStream.js.map +1 -1
- package/dist/persistence/rpc-allowlist.d.ts.map +1 -1
- package/dist/persistence/rpc-allowlist.js +1 -0
- package/dist/persistence/rpc-allowlist.js.map +1 -1
- 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
|
|
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.
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
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":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;
|
|
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":"
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
962
|
+
.map((p) => toPublicPipeline(p, admissionRegistries(container))),
|
|
980
963
|
}, 200);
|
|
981
964
|
});
|
|
982
965
|
}
|