@defrex/autobuild 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/SPEC.md +237 -23
- package/bin/ab-dispatch-kernel.ts +1 -1
- package/docs/architecture.md +66 -4
- package/docs/configuration.md +137 -19
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +214 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +107 -7
- package/docs/remote-store-protocol.md +189 -144
- package/docs/remote-ticket-protocol.md +111 -18
- package/docs/setup.md +173 -16
- package/docs/ticket-dependencies.md +1 -1
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/binary.ts +3 -3
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +261 -0
- package/packages/core/src/cli/dashboard/model.ts +8 -5
- package/packages/core/src/cli/dashboard/render.ts +37 -7
- package/packages/core/src/cli/dispatch-frontend.ts +16 -1
- package/packages/core/src/cli/dispatch-process.ts +25 -7
- package/packages/core/src/cli/dispatch.ts +247 -47
- package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
- package/packages/core/src/cli/help.ts +45 -13
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/main.ts +41 -15
- package/packages/core/src/cli/mcp.ts +7 -1
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/repository-status.ts +45 -5
- package/packages/core/src/cli/self-update.ts +10 -0
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket.ts +8 -1
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +350 -68
- package/packages/core/src/config/live.ts +4 -0
- package/packages/core/src/config/schema.ts +65 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +10 -2
- package/packages/core/src/events/globs.ts +8 -3
- package/packages/core/src/events/payloads.ts +44 -0
- package/packages/core/src/events/repository.ts +64 -2
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +148 -83
- package/packages/core/src/kernel/dispatch-status.ts +9 -1
- package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +10 -0
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +12 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/kernel/ticket-source-health.ts +30 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/query.ts +44 -19
- package/packages/core/src/operator/ticket-source-backend.ts +1 -0
- package/packages/core/src/operator/tickets.ts +6 -8
- package/packages/core/src/plugin-sdk/index.ts +1 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +3 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +1 -0
- package/packages/core/src/ports/tickets/contract.ts +25 -0
- package/packages/core/src/ports/tickets/create.ts +11 -3
- package/packages/core/src/ports/tickets/fake.ts +10 -8
- package/packages/core/src/ports/tickets/file.ts +1 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
- package/packages/core/src/ports/tickets/linear.ts +241 -49
- package/packages/core/src/ports/tickets/remote-protocol.ts +3 -0
- package/packages/core/src/ports/tickets/remote.ts +1 -0
- package/packages/core/src/ports/types.ts +17 -0
- package/packages/core/src/ports/workspace/build-execution.ts +5 -1
- package/packages/core/src/ports/workspace/create.ts +11 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +336 -83
- package/packages/core/src/processes/harvest-child.ts +1 -0
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +142 -100
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +2 -1
- package/packages/core/src/store/adapter.ts +1 -0
- package/packages/core/src/store/contract.ts +109 -0
- package/packages/core/src/store/digest.ts +14 -2
- package/packages/core/src/store/local/store.ts +18 -5
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +26 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/store/repo-state-events.ts +49 -59
- package/packages/core/src/store/types.ts +5 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +175 -26
- package/skills/guide/references/operator-api.md +135 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +189 -144
- package/skills/guide/references/remote-ticket-protocol.md +111 -18
- package/skills/guide/references/setup.md +173 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/operate/SKILL.md +159 -34
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import semver from 'semver'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { AUTOBUILD_VERSION, REMOTE_STORE_PROTOCOL_VERSION } from './version'
|
|
4
|
+
|
|
5
|
+
/** Stable releases only: prefixes, whitespace, prereleases and build suffixes are not identities. */
|
|
6
|
+
export function stableVersion(value: unknown): semver.SemVer | undefined {
|
|
7
|
+
if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/.test(value)) return undefined
|
|
8
|
+
const parsed = semver.parse(value)
|
|
9
|
+
return parsed && parsed.version === value ? parsed : undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const stableVersionSchema = z.string().refine((value) => stableVersion(value) !== undefined)
|
|
13
|
+
export const supportedClientRangeSchema = z
|
|
14
|
+
.strictObject({
|
|
15
|
+
minVersion: stableVersionSchema,
|
|
16
|
+
maxVersionExclusive: stableVersionSchema,
|
|
17
|
+
})
|
|
18
|
+
.refine((range) => semver.lt(range.minVersion, range.maxVersionExclusive))
|
|
19
|
+
export type SupportedClientRange = z.infer<typeof supportedClientRangeSchema>
|
|
20
|
+
|
|
21
|
+
/** A major starts a new family: X.0 has one line, X.1 two, X.2+ three. */
|
|
22
|
+
export function supportedClientRange(serviceVersion = AUTOBUILD_VERSION): SupportedClientRange {
|
|
23
|
+
const version = stableVersion(serviceVersion)
|
|
24
|
+
if (!version) throw new Error('service must have a stable release version')
|
|
25
|
+
return {
|
|
26
|
+
minVersion: `${version.major}.${Math.max(0, version.minor - 2)}.0`,
|
|
27
|
+
maxVersionExclusive: `${version.major}.${version.minor + 1}.0`,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function classifyClient(
|
|
32
|
+
version: unknown,
|
|
33
|
+
range: SupportedClientRange,
|
|
34
|
+
): 'supported' | 'older' | 'newer' | 'invalid' {
|
|
35
|
+
const parsed = stableVersion(version)
|
|
36
|
+
if (!parsed) return 'invalid'
|
|
37
|
+
if (semver.lt(parsed, range.minVersion)) return 'older'
|
|
38
|
+
if (semver.gte(parsed, range.maxVersionExclusive)) return 'newer'
|
|
39
|
+
return 'supported'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatClientRange(range: SupportedClientRange): string {
|
|
43
|
+
return `>=${range.minVersion} <${range.maxVersionExclusive}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function compatibilityAdvice(
|
|
47
|
+
client: unknown,
|
|
48
|
+
serviceVersion: string,
|
|
49
|
+
range: SupportedClientRange,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
const classification = classifyClient(client, range)
|
|
52
|
+
const description = `service Autobuild ${serviceVersion} supports clients ${formatClientRange(range)}`
|
|
53
|
+
if (classification === 'supported') return undefined
|
|
54
|
+
if (classification === 'newer') {
|
|
55
|
+
const version = stableVersion(client)!
|
|
56
|
+
return `${description}; upgrade the service to at least ${version.major}.${version.minor}.0 before upgrading projects (service first)`
|
|
57
|
+
}
|
|
58
|
+
if (classification === 'older')
|
|
59
|
+
return `${description}; upgrade the project's Autobuild into ${formatClientRange(range)}`
|
|
60
|
+
return `${description}; supply a stable Autobuild client version`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Retain these wire identities and handlers until their lines leave the window.
|
|
64
|
+
* Historical lines require explicit entries; the service's own line uses its
|
|
65
|
+
* current protocol identity. Unknown historical protocols fail closed. */
|
|
66
|
+
export const RELEASE_LINE_PROTOCOLS: Readonly<Record<string, string>> = {
|
|
67
|
+
'0.9': '3',
|
|
68
|
+
'0.10': '3',
|
|
69
|
+
'0.11': '3',
|
|
70
|
+
'0.12': '3',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function clientIdentityError(
|
|
74
|
+
client: string | null,
|
|
75
|
+
protocol: string | null,
|
|
76
|
+
serviceVersion = AUTOBUILD_VERSION,
|
|
77
|
+
serviceProtocol = REMOTE_STORE_PROTOCOL_VERSION,
|
|
78
|
+
): string | undefined {
|
|
79
|
+
const range = supportedClientRange(serviceVersion)
|
|
80
|
+
const service = stableVersion(serviceVersion)!
|
|
81
|
+
const advice = compatibilityAdvice(client, serviceVersion, range)
|
|
82
|
+
const parsed = stableVersion(client)
|
|
83
|
+
const expected =
|
|
84
|
+
parsed &&
|
|
85
|
+
(parsed.major === service.major && parsed.minor === service.minor
|
|
86
|
+
? serviceProtocol
|
|
87
|
+
: RELEASE_LINE_PROTOCOLS[`${parsed.major}.${parsed.minor}`])
|
|
88
|
+
if (!advice && expected !== undefined && protocol === expected) return undefined
|
|
89
|
+
return `remote store version mismatch: client Autobuild ${client ?? '(missing)'} protocol ${protocol ?? '(missing)'}; server Autobuild ${serviceVersion} protocol ${serviceProtocol}; ${advice ?? `supported clients ${formatClientRange(range)}; unsupported protocol identity`}`
|
|
90
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { delegateCredentialResponseSchema, type DelegateCredentialTarget } from './protocol'
|
|
2
|
+
import {
|
|
3
|
+
AUTOBUILD_VERSION,
|
|
4
|
+
AUTOBUILD_VERSION_HEADER,
|
|
5
|
+
REMOTE_STORE_PROTOCOL_VERSION,
|
|
6
|
+
REMOTE_STORE_PROTOCOL_VERSION_HEADER,
|
|
7
|
+
} from './version'
|
|
8
|
+
|
|
9
|
+
/** Fail closed: a remote execution must never fall back to its parent token. */
|
|
10
|
+
export async function delegateCredential(
|
|
11
|
+
storeRef: string,
|
|
12
|
+
token: string | undefined,
|
|
13
|
+
target: DelegateCredentialTarget,
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
if (!token) throw new Error('credential delegation requires parent authority')
|
|
16
|
+
const response = await fetch(`${storeRef.replace(/\/$/, '')}/credentials/delegate`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
authorization: `Bearer ${token}`,
|
|
20
|
+
'content-type': 'application/json',
|
|
21
|
+
[AUTOBUILD_VERSION_HEADER]: AUTOBUILD_VERSION,
|
|
22
|
+
[REMOTE_STORE_PROTOCOL_VERSION_HEADER]: REMOTE_STORE_PROTOCOL_VERSION,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(target),
|
|
25
|
+
})
|
|
26
|
+
if (!response.ok) throw new Error(`credential delegation refused (${response.status})`)
|
|
27
|
+
return delegateCredentialResponseSchema.parse(await response.json()).token
|
|
28
|
+
}
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
export { AuthError, RemoteBuildStore } from './client'
|
|
5
5
|
export type { RemoteBuildStoreOptions, RemoteStoreIdentity } from './client'
|
|
6
6
|
export { mintToken, tokenResource, verifyToken } from './token'
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
OperatorTokenScope,
|
|
9
|
+
InternalOperatorTokenScope,
|
|
10
|
+
AdminTokenScope,
|
|
11
|
+
TokenScope,
|
|
12
|
+
DelegatedTokenScope,
|
|
13
|
+
} from './token'
|
|
8
14
|
export {
|
|
9
15
|
AUTOBUILD_VERSION,
|
|
10
16
|
AUTOBUILD_VERSION_HEADER,
|
|
@@ -22,3 +28,10 @@ export type { SessionEvent, SessionEventWrite } from '../../events/sessions'
|
|
|
22
28
|
export type { RepositoryEvent, RepositoryEventWrite } from '../../events/repository'
|
|
23
29
|
export { SNAPSHOT_REDUCER_NAME } from '../snapshots'
|
|
24
30
|
export type { ReducerSnapshot, SnapshotScope } from '../snapshots'
|
|
31
|
+
|
|
32
|
+
export * from './compatibility'
|
|
33
|
+
|
|
34
|
+
export { delegateCredential } from './credentials'
|
|
35
|
+
|
|
36
|
+
export { reduceBuild } from '../../kernel/reducer'
|
|
37
|
+
export { reduceHarvest } from '../../kernel/harvest'
|
|
@@ -285,6 +285,7 @@ export const buildDigestWireSchema = z.object({
|
|
|
285
285
|
}),
|
|
286
286
|
),
|
|
287
287
|
merged: z.string().min(1).optional(),
|
|
288
|
+
readinessChangeSeq: z.number().int().positive().optional(),
|
|
288
289
|
reclaimPending: z.literal(true).optional(),
|
|
289
290
|
executionOpen: z.literal(true).optional(),
|
|
290
291
|
})
|
|
@@ -449,3 +450,28 @@ export function substitutePlaceholderRefs(
|
|
|
449
450
|
}
|
|
450
451
|
return value
|
|
451
452
|
}
|
|
453
|
+
|
|
454
|
+
/** Issuance is an authentication operation, separate from the BuildStore port. */
|
|
455
|
+
export const delegateCredentialBodySchema = z.discriminatedUnion('kind', [
|
|
456
|
+
z.strictObject({ kind: z.literal('build-runner'), id: z.string().min(1) }),
|
|
457
|
+
z.strictObject({
|
|
458
|
+
kind: z.literal('build-phase'),
|
|
459
|
+
id: z.string().min(1),
|
|
460
|
+
session: z.string().min(1),
|
|
461
|
+
}),
|
|
462
|
+
z.strictObject({
|
|
463
|
+
kind: z.literal('harvest-runner'),
|
|
464
|
+
execution: z.string().min(1),
|
|
465
|
+
repo: z.string().min(1),
|
|
466
|
+
id: z.string().min(1),
|
|
467
|
+
leaseHolder: z.string().min(1),
|
|
468
|
+
}),
|
|
469
|
+
z.strictObject({
|
|
470
|
+
kind: z.literal('harvest-phase'),
|
|
471
|
+
repo: z.string().min(1),
|
|
472
|
+
id: z.string().min(1),
|
|
473
|
+
session: z.string().min(1),
|
|
474
|
+
}),
|
|
475
|
+
])
|
|
476
|
+
export type DelegateCredentialTarget = z.infer<typeof delegateCredentialBodySchema>
|
|
477
|
+
export const delegateCredentialResponseSchema = z.strictObject({ token: z.string().min(1) })
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* runner minting a narrow token, not from prompt instructions.
|
|
6
|
-
*
|
|
7
|
-
* A token is `base64url(JSON scope) + '.' + base64url(hmac-sha256(secret,
|
|
8
|
-
* payload))`. The scope IS the token — the server keeps no session state;
|
|
9
|
-
* it verifies the signature (timing-safe) and the expiry, then trusts the
|
|
10
|
-
* embedded scope.
|
|
1
|
+
/** Signed lifecycle-bound execution grants, internal owner delegation and admin.
|
|
2
|
+
* Historical resource/deployment/operator scopes remain decodable for tests
|
|
3
|
+
* and migration diagnostics; hosted machine gates reject those retired forms.
|
|
4
|
+
* Project credentials are opaque stored secrets, not these stateless tokens.
|
|
11
5
|
*/
|
|
12
6
|
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
13
7
|
import { z } from 'zod'
|
|
@@ -52,8 +46,35 @@ export interface DeploymentTokenScope {
|
|
|
52
46
|
exp: number
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
/**
|
|
49
|
+
/** Includes retired wire shapes for recognition, never server authorization. */
|
|
50
|
+
export interface InternalOperatorTokenScope {
|
|
51
|
+
version: 1
|
|
52
|
+
internalOperator: { user: string; repo: string }
|
|
53
|
+
exp: number
|
|
54
|
+
via?: Via
|
|
55
|
+
}
|
|
56
|
+
export interface AdminTokenScope {
|
|
57
|
+
version: 1
|
|
58
|
+
ownerAdmin: { user: string }
|
|
59
|
+
exp: number
|
|
60
|
+
}
|
|
61
|
+
export interface DelegatedTokenScope {
|
|
62
|
+
version: 2
|
|
63
|
+
delegation: {
|
|
64
|
+
repo: string
|
|
65
|
+
kind: 'build-runner' | 'build-phase' | 'harvest-runner' | 'harvest-phase'
|
|
66
|
+
id: string
|
|
67
|
+
anchor: number
|
|
68
|
+
session?: string
|
|
69
|
+
execution?: string
|
|
70
|
+
leaseHolder?: string
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
export type TokenScope =
|
|
75
|
+
| DelegatedTokenScope
|
|
76
|
+
| InternalOperatorTokenScope
|
|
77
|
+
| AdminTokenScope
|
|
57
78
|
| LegacyBuildTokenScope
|
|
58
79
|
| ResourceTokenScope
|
|
59
80
|
| OperatorTokenScope
|
|
@@ -63,7 +84,38 @@ const commonScope = {
|
|
|
63
84
|
session: z.string().min(1),
|
|
64
85
|
exp: z.number().int(),
|
|
65
86
|
}
|
|
87
|
+
const exactIdentity = z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.refine((value) => value !== '*')
|
|
91
|
+
const delegatedClaims = z
|
|
92
|
+
.strictObject({
|
|
93
|
+
repo: exactIdentity,
|
|
94
|
+
kind: z.enum(['build-runner', 'build-phase', 'harvest-runner', 'harvest-phase']),
|
|
95
|
+
id: exactIdentity,
|
|
96
|
+
anchor: z.number().int().positive(),
|
|
97
|
+
session: exactIdentity.optional(),
|
|
98
|
+
execution: exactIdentity.optional(),
|
|
99
|
+
leaseHolder: exactIdentity.optional(),
|
|
100
|
+
})
|
|
101
|
+
.refine(
|
|
102
|
+
(claims) =>
|
|
103
|
+
claims.kind.endsWith('-phase') === (claims.session !== undefined) &&
|
|
104
|
+
claims.kind.startsWith('harvest') === (claims.execution !== undefined),
|
|
105
|
+
)
|
|
66
106
|
const tokenScopeSchema = z.union([
|
|
107
|
+
z.strictObject({ version: z.literal(2), delegation: delegatedClaims }),
|
|
108
|
+
z.strictObject({
|
|
109
|
+
version: z.literal(1),
|
|
110
|
+
internalOperator: z.strictObject({ user: z.string().trim().min(1), repo: z.string().min(1) }),
|
|
111
|
+
exp: z.number().int(),
|
|
112
|
+
via: viaSchema.optional(),
|
|
113
|
+
}),
|
|
114
|
+
z.strictObject({
|
|
115
|
+
version: z.literal(1),
|
|
116
|
+
ownerAdmin: z.strictObject({ user: z.string().trim().min(1) }),
|
|
117
|
+
exp: z.number().int(),
|
|
118
|
+
}),
|
|
67
119
|
z.strictObject({ build: z.string().min(1), ...commonScope }),
|
|
68
120
|
z.strictObject({
|
|
69
121
|
resource: z.strictObject({
|
|
@@ -85,6 +137,13 @@ export function tokenResource(scope: TokenScope): {
|
|
|
85
137
|
kind: 'build' | 'repo' | 'session' | 'admin' | 'operator' | 'deployment'
|
|
86
138
|
id: string
|
|
87
139
|
} {
|
|
140
|
+
if ('delegation' in scope)
|
|
141
|
+
return {
|
|
142
|
+
kind: scope.delegation.kind.startsWith('build') ? 'build' : 'repo',
|
|
143
|
+
id: scope.delegation.kind.startsWith('build') ? scope.delegation.id : scope.delegation.repo,
|
|
144
|
+
}
|
|
145
|
+
if ('ownerAdmin' in scope) return { kind: 'admin', id: '*' }
|
|
146
|
+
if ('internalOperator' in scope) return { kind: 'operator', id: scope.internalOperator.user }
|
|
88
147
|
if ('operator' in scope) {
|
|
89
148
|
return scope.operator === true
|
|
90
149
|
? { kind: 'deployment', id: '*' }
|
|
@@ -108,7 +167,9 @@ export function mintToken(secret: string, scope: TokenScope): string {
|
|
|
108
167
|
/**
|
|
109
168
|
* Bad signature, malformed token, unparseable scope, or expired → null —
|
|
110
169
|
* verification never explains itself to the caller (the server maps null to
|
|
111
|
-
* 401).
|
|
170
|
+
* 401). Version-2 grants deliberately have no fixed expiration: the service
|
|
171
|
+
* must additionally validate their registered project and durable lifecycle.
|
|
172
|
+
* The signature compare is timing-safe via `crypto.timingSafeEqual`.
|
|
112
173
|
*/
|
|
113
174
|
export function verifyToken(secret: string, token: string, now: Date): TokenScope | null {
|
|
114
175
|
const parts = token.split('.')
|
|
@@ -132,6 +193,6 @@ export function verifyToken(secret: string, token: string, now: Date): TokenScop
|
|
|
132
193
|
}
|
|
133
194
|
const scope = tokenScopeSchema.safeParse(raw)
|
|
134
195
|
if (!scope.success) return null
|
|
135
|
-
if (scope.data.exp <= now.getTime()) return null
|
|
196
|
+
if ('exp' in scope.data && scope.data.exp <= now.getTime()) return null
|
|
136
197
|
return scope.data
|
|
137
198
|
}
|
|
@@ -1,54 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The hosted dispatcher appends ~4 journal events per invocation (run-started,
|
|
7
|
-
* tick-started, tick-completed/tick-yielded, run-stopped) whether or not it
|
|
8
|
-
* did anything, so the journal grows forever with wall-clock time while the
|
|
9
|
-
* durable signal stays small. Every repository-state consumer reduces the
|
|
10
|
-
* journal through one of four pure reducers whose event-type dependencies
|
|
11
|
-
* partition the catalog:
|
|
12
|
-
*
|
|
13
|
-
* - `reduceDispatchSettings` — only the three dispatcher setting types
|
|
14
|
-
* (latest-wins by seq).
|
|
15
|
-
* - `reduceDispatchStatus(events, latestRun)` — only run-correlated
|
|
16
|
-
* dispatcher facts, and it skips every event whose `payload.run` is not the
|
|
17
|
-
* latest run. A run's facts are appended under its own run id, which is
|
|
18
|
-
* minted at `dispatcher.run-started`, so all facts the reducer keeps live at
|
|
19
|
-
* or after the latest run-started.
|
|
20
|
-
* - `reduceHarvest` (and everything built on it) — only `harvest.*` events
|
|
21
|
-
* plus the three setting types, which it explicitly ignores.
|
|
22
|
-
* - `sandboxStates` — only `orchestrator.sandbox.*` events.
|
|
23
|
-
*
|
|
24
|
-
* So the bounded subset is: **every event of a durable (slow-growing) type —
|
|
25
|
-
* all `harvest.*`, all `orchestrator.sandbox.*`, and the three dispatcher
|
|
26
|
-
* setting types — plus, when the journal contains at least one
|
|
27
|
-
* `dispatcher.run-started`, the tail from that latest run-started to the end
|
|
28
|
-
* of the journal.** For a hosted repository with N no-op invocations this
|
|
29
|
-
* subset's size is a constant (one invocation's ~4 facts plus the durable
|
|
30
|
-
* signal), independent of N.
|
|
31
|
-
*
|
|
32
|
-
* INVARIANT FOR AUTHORS OF NEW REPOSITORY EVENT TYPES: a new type must either
|
|
33
|
-
* (a) join `REPOSITORY_STATE_EVENT_TYPES` — it is durable, slow-growing state
|
|
34
|
-
* some reducer depends on at full replay depth — or (b) join
|
|
35
|
-
* `REPOSITORY_RUN_SCOPED_EVENT_TYPES` — it is run-correlated and its only
|
|
36
|
-
* consumers reduce it for the latest run, so the tail from the latest
|
|
37
|
-
* `dispatcher.run-started` covers it. `tests/store/repo-state-events.test.ts`
|
|
38
|
-
* asserts the two sets exactly partition `REPOSITORY_EVENT_TYPES`; adding a
|
|
39
|
-
* type to neither fails the build until the author makes the call. A type in
|
|
40
|
-
* neither set would be silently dropped from bounded reads.
|
|
41
|
-
*
|
|
42
|
-
* Reducing this subset with the state readers yields exactly what a full
|
|
43
|
-
* replay yields: `reduceDispatchSettings`, `reduceHarvest`, `sandboxStates`
|
|
44
|
-
* see identical event lists, and `reduceDispatchStatus` sees every event it
|
|
45
|
-
* keeps (the rest are skipped by the `payload.run !== latestRun` guard). The
|
|
46
|
-
* journal itself stays append-only; no event is deleted, rewritten, or
|
|
47
|
-
* re-ordered, and a replay from the start is untouched.
|
|
1
|
+
/** Bounded journal state has three disjoint, exhaustive categories:
|
|
2
|
+
* durable facts retain all history; run-scoped facts retain the latest run
|
|
3
|
+
* tail; latest-only facts retain exactly the greatest sequence per type,
|
|
4
|
+
* including before any run anchor. The full journal remains append-only.
|
|
48
5
|
*/
|
|
49
6
|
import type { RepositoryEvent, RepositoryEventType } from '../events/repository'
|
|
50
7
|
import { defineReducer, type IncrementalReducer } from '../kernel/incremental'
|
|
51
8
|
import {
|
|
9
|
+
ticketSourceHealthEventPayloadSchemas,
|
|
52
10
|
dispatcherSettingEventPayloadSchemas,
|
|
53
11
|
dispatcherStatusEventPayloadSchemas,
|
|
54
12
|
harvestEventPayloadSchemas,
|
|
@@ -64,6 +22,7 @@ const RUN_STARTED = 'dispatcher.run-started'
|
|
|
64
22
|
* setting types are enumerated because they share one schema group with the
|
|
65
23
|
* run-correlated `dispatcher.config-reloaded`. */
|
|
66
24
|
export const REPOSITORY_STATE_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
25
|
+
...Object.keys(ticketSourceHealthEventPayloadSchemas),
|
|
67
26
|
...Object.keys(harvestEventPayloadSchemas),
|
|
68
27
|
...Object.keys(orchestratorSandboxEventPayloadSchemas),
|
|
69
28
|
...Object.keys(dispatcherSettingEventPayloadSchemas).filter(
|
|
@@ -71,11 +30,19 @@ export const REPOSITORY_STATE_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
|
71
30
|
),
|
|
72
31
|
] as RepositoryEventType[]
|
|
73
32
|
|
|
33
|
+
/** Latest-only measurements survive run turnover without retaining scan history. */
|
|
34
|
+
export const REPOSITORY_LATEST_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
35
|
+
'dispatcher.ticket-scan-completed',
|
|
36
|
+
]
|
|
37
|
+
const LATEST_TYPES = new Set<string>(REPOSITORY_LATEST_EVENT_TYPES)
|
|
38
|
+
|
|
74
39
|
/** Run-correlated repository event types: facts whose only state consumers
|
|
75
40
|
* reduce them for the latest dispatcher run, so the tail from the latest
|
|
76
41
|
* `dispatcher.run-started` contains everything those consumers keep. */
|
|
77
42
|
export const REPOSITORY_RUN_SCOPED_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
78
|
-
...Object.keys(dispatcherStatusEventPayloadSchemas)
|
|
43
|
+
...Object.keys(dispatcherStatusEventPayloadSchemas).filter(
|
|
44
|
+
(type) => !REPOSITORY_LATEST_EVENT_TYPES.includes(type as RepositoryEventType),
|
|
45
|
+
),
|
|
79
46
|
'dispatcher.config-reloaded',
|
|
80
47
|
] as RepositoryEventType[]
|
|
81
48
|
|
|
@@ -122,8 +89,8 @@ export async function readRepoStateEventsWithAnchorRecheck(
|
|
|
122
89
|
}
|
|
123
90
|
|
|
124
91
|
/** Carried state for the bounded read: the latest run-started seq (the last
|
|
125
|
-
* in array order, as the whole-array pass overwrites it) and every event seen,
|
|
126
|
-
* in array order.
|
|
92
|
+
* in array order, as the whole-array pass overwrites it) and every ordinary event seen,
|
|
93
|
+
* in array order, plus one latest-only fact per type. Ordinary history is retained: a later run-started may carry a lower seq
|
|
127
94
|
* than an earlier one and restore events below the earlier anchor, so only
|
|
128
95
|
* `finish` can apply the filter exactly. Pruning below a monotone anchor is
|
|
129
96
|
* valid only for seq-ordered input and belongs with the cached-state
|
|
@@ -134,10 +101,11 @@ export interface RepositoryStateEventsAcc {
|
|
|
134
101
|
}
|
|
135
102
|
|
|
136
103
|
/** Bump when `RepositoryStateEventsAcc` or its fold changes. */
|
|
137
|
-
export const REPOSITORY_STATE_EVENTS_REDUCER_VERSION =
|
|
104
|
+
export const REPOSITORY_STATE_EVENTS_REDUCER_VERSION = 4
|
|
138
105
|
|
|
139
106
|
const keeps = (event: RepositoryEvent, anchor: number | undefined): boolean =>
|
|
140
|
-
STATE_TYPES.has(event.type) ||
|
|
107
|
+
STATE_TYPES.has(event.type) ||
|
|
108
|
+
(!LATEST_TYPES.has(event.type) && anchor !== undefined && event.seq >= anchor)
|
|
141
109
|
|
|
142
110
|
export const repositoryStateEventsReducer: IncrementalReducer<
|
|
143
111
|
RepositoryStateEventsAcc,
|
|
@@ -149,10 +117,24 @@ export const repositoryStateEventsReducer: IncrementalReducer<
|
|
|
149
117
|
fold(acc, events) {
|
|
150
118
|
for (const event of events) {
|
|
151
119
|
if (event.type === RUN_STARTED) acc.anchor = event.seq
|
|
120
|
+
if (LATEST_TYPES.has(event.type)) {
|
|
121
|
+
const held = acc.retained.find((held) => held.type === event.type)
|
|
122
|
+
if (held !== undefined && held.seq >= event.seq) continue
|
|
123
|
+
acc.retained = acc.retained.filter((held) => held.type !== event.type)
|
|
124
|
+
}
|
|
152
125
|
acc.retained.push(event)
|
|
153
126
|
}
|
|
154
127
|
},
|
|
155
|
-
finish: (acc) =>
|
|
128
|
+
finish: (acc) => {
|
|
129
|
+
const latest = new Map<string, RepositoryEvent>()
|
|
130
|
+
for (const event of acc.retained) {
|
|
131
|
+
if (LATEST_TYPES.has(event.type) && event.seq > (latest.get(event.type)?.seq ?? -1))
|
|
132
|
+
latest.set(event.type, event)
|
|
133
|
+
}
|
|
134
|
+
return [...acc.retained.filter((event) => keeps(event, acc.anchor)), ...latest.values()].sort(
|
|
135
|
+
(a, b) => a.seq - b.seq,
|
|
136
|
+
)
|
|
137
|
+
},
|
|
156
138
|
})
|
|
157
139
|
|
|
158
140
|
/** Carried state for the pruned journal view: the latest run-started seq and
|
|
@@ -167,16 +149,23 @@ export interface JournalViewAcc {
|
|
|
167
149
|
}
|
|
168
150
|
|
|
169
151
|
/** Bump when `JournalViewAcc` or its fold changes. */
|
|
170
|
-
export const JOURNAL_VIEW_REDUCER_VERSION =
|
|
152
|
+
export const JOURNAL_VIEW_REDUCER_VERSION = 4
|
|
171
153
|
|
|
172
154
|
/** Fold seq-ordered events into a journal view in place. */
|
|
173
155
|
export function foldJournalView(acc: JournalViewAcc, events: readonly RepositoryEvent[]): void {
|
|
174
156
|
for (const event of events) {
|
|
175
157
|
if (event.type === RUN_STARTED) {
|
|
176
158
|
acc.anchor = event.seq
|
|
177
|
-
acc.retained = acc.retained.filter(
|
|
159
|
+
acc.retained = acc.retained.filter(
|
|
160
|
+
(held) => STATE_TYPES.has(held.type) || LATEST_TYPES.has(held.type),
|
|
161
|
+
)
|
|
178
162
|
}
|
|
179
|
-
if (
|
|
163
|
+
if (LATEST_TYPES.has(event.type)) {
|
|
164
|
+
const held = acc.retained.find((held) => held.type === event.type)
|
|
165
|
+
if (held !== undefined && held.seq >= event.seq) continue
|
|
166
|
+
acc.retained = acc.retained.filter((held) => held.type !== event.type)
|
|
167
|
+
acc.retained.push(event)
|
|
168
|
+
} else if (STATE_TYPES.has(event.type) || acc.anchor !== undefined) acc.retained.push(event)
|
|
180
169
|
}
|
|
181
170
|
}
|
|
182
171
|
|
|
@@ -193,9 +182,10 @@ export const journalViewReducer: IncrementalReducer<
|
|
|
193
182
|
|
|
194
183
|
/** The normative subset derivation every adapter implements and the contract
|
|
195
184
|
* tests use as the oracle: durable types across the whole journal, plus —
|
|
196
|
-
* only when the journal has one — every event from the latest
|
|
197
|
-
* `dispatcher.run-started` onward,
|
|
198
|
-
*
|
|
185
|
+
* only when the journal has one — every non-latest-only event from the latest
|
|
186
|
+
* `dispatcher.run-started` onward, plus the maximum-sequence fact of each
|
|
187
|
+
* latest-only type across the whole journal, ordered by seq. A journal with no
|
|
188
|
+
* run-started has **no tail at all** (durable and latest-only facts only); the anchor is
|
|
199
189
|
* never defaulted to 0, which would make a `seq >= anchor` clause select the
|
|
200
190
|
* whole journal and defeat the bound. */
|
|
201
191
|
export function projectRepositoryStateEvents(
|
|
@@ -105,6 +105,8 @@ export interface ArtifactMeta {
|
|
|
105
105
|
* the event log on each call, so the log stays the sole authority and the
|
|
106
106
|
* digest can always be discarded and rebuilt. */
|
|
107
107
|
export interface BuildDigest {
|
|
108
|
+
/** Latest merge, completion or abort fact sequence. */
|
|
109
|
+
readinessChangeSeq?: number
|
|
108
110
|
slug: string
|
|
109
111
|
terminal?: 'done' | 'aborted'
|
|
110
112
|
/** Ascending-seq `{seq, ts}` pairs for every `observation.recorded` in the
|
|
@@ -368,7 +370,9 @@ export interface BuildStore {
|
|
|
368
370
|
): Promise<RepositoryEvent[]>
|
|
369
371
|
/** Bounded repository-journal read for stateless full-state readers
|
|
370
372
|
* (AUT-489): every durable fact (REPOSITORY_STATE_EVENT_TYPES) plus — when
|
|
371
|
-
* the journal has one — the tail from the latest `dispatcher.run-started
|
|
373
|
+
* the journal has one — the tail from the latest `dispatcher.run-started`,
|
|
374
|
+
* excluding latest-only types; plus exactly the maximum-sequence fact for
|
|
375
|
+
* each REPOSITORY_LATEST_EVENT_TYPES type across the whole journal.
|
|
372
376
|
* Reducing this subset with the state readers yields exactly what a full
|
|
373
377
|
* replay yields (`projectRepositoryStateEvents` is the normative oracle).
|
|
374
378
|
* Unknown repo rejects like `getRepoEvents`. */
|
|
@@ -57,3 +57,10 @@ export {
|
|
|
57
57
|
type E2eHarness,
|
|
58
58
|
type SkillHandlers,
|
|
59
59
|
} from '../integration/harness'
|
|
60
|
+
|
|
61
|
+
// Harvest lifecycle and CLI seams for delegated-credential integration tests.
|
|
62
|
+
export { HarvestRunner } from '../processes/harvest-runner'
|
|
63
|
+
export { resolveHarvestCliEnv } from '../cli/env'
|
|
64
|
+
export { buildHarvestContext, submitHarvestProposals, submitHarvestVerdict } from '../cli/harvest'
|
|
65
|
+
export { abConnect, abDisconnect } from '../cli/connect'
|
|
66
|
+
export { ProjectCredentials } from '../cli/project-credentials'
|