diff --git a/.env.example b/.env.example index b16835f2..751c7b5c 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,14 @@ ARTIFACT_SERVER_ORIGIN=https://artifactserver.example # ARTIFACT_SERVER_OIDC_CLIENT_SECRET=replace_me # ARTIFACT_SERVER_OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc-client-secret # ARTIFACT_SERVER_OIDC_SCOPES=openid email profile +# Automatic member admission after a validated OIDC browser login is off by default. +# Use exactly true or false. With true, an empty domain list trusts the configured +# issuer's access restrictions; a list permits new members only from exact email +# domains (case-insensitive, no wildcards or implicit subdomains). Existing members +# keep their roles, and deactivated identities stay denied. Bootstrap the first +# administrator before other users sign in. These settings do not admit MCP users. +# ARTIFACT_SERVER_OIDC_AUTO_ADMIT=false +# ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS=example.com,example.org # External-storage runtime. The bucket must already exist. Leave endpoint unset for # AWS S3; set it and force path style only for a compatible provider that needs it. diff --git a/deploy/cloudflare/src/d1-identity-repository.ts b/deploy/cloudflare/src/d1-identity-repository.ts index 3a67935b..bbbacc9a 100644 --- a/deploy/cloudflare/src/d1-identity-repository.ts +++ b/deploy/cloudflare/src/d1-identity-repository.ts @@ -419,6 +419,29 @@ export function createD1IdentityRepository( findMember, + findMemberByEmail: async (installationId, email) => { + const row = await database.prepare(`${memberSelect} + WHERE installation_id = ? AND email = ? + `).bind(installationId, email) + .first>(); + return row === null ? null : memberRowSchema.parse(row); + }, + + findMemberByExternalIdentity: async (installationId, provider, subject) => { + const row = await database.prepare(` + SELECT member.id, member.installation_id AS installationId, + member.email, member.display_name AS displayName, member.role, + member.status, member.created_at AS createdAt, + member.updated_at AS updatedAt + FROM external_identities AS external + JOIN installation_members AS member ON member.id = external.member_id + WHERE member.installation_id = ? AND external.provider = ? + AND external.subject = ? + `).bind(installationId, provider, subject) + .first>(); + return row === null ? null : memberRowSchema.parse(row); + }, + hasMembers: async (installationId) => { const row = await database.prepare(` SELECT EXISTS( diff --git a/deploy/cloudflare/src/worker.ts b/deploy/cloudflare/src/worker.ts index 1db4a9e9..fd135535 100644 --- a/deploy/cloudflare/src/worker.ts +++ b/deploy/cloudflare/src/worker.ts @@ -45,6 +45,8 @@ import { } from "../../../src/identity/oidc-identity-provider.js"; import {createWorkOsHostedAuthentication} from "../../../src/identity/workos-hosted-authentication.js"; +import {parseOidcAdmissionPolicy} from + "../../../src/identity/oidc-admission-policy.js"; /** Bindings and variables the deployed Artifact Server worker reads. */ export interface WorkerEnvironment { @@ -57,6 +59,8 @@ export interface WorkerEnvironment { readonly ARTIFACT_SERVER_CLOUDFLARE_ARTIFACTS_NAMESPACE?: string; readonly ARTIFACT_SERVER_D1_DATABASE: D1Database; readonly ARTIFACT_SERVER_INSTALLATION_ID: string; + readonly ARTIFACT_SERVER_OIDC_AUTO_ADMIT?: string; + readonly ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS?: string; readonly ARTIFACT_SERVER_OIDC_CLIENT_ID?: string; readonly ARTIFACT_SERVER_OIDC_CLIENT_SECRET?: string; readonly ARTIFACT_SERVER_OIDC_ISSUER?: string; @@ -142,6 +146,13 @@ async function createCloudflareRuntime( environment: WorkerEnvironment, ): Promise { validateEnvironment(environment); + const oidcIdentityProvider = oidcAuthentication(environment); + const admissionPolicy = oidcIdentityProvider === null ? null : + parseOidcAdmissionPolicy( + requireEnvironmentValue(environment.ARTIFACT_SERVER_OIDC_ISSUER), + environment.ARTIFACT_SERVER_OIDC_AUTO_ADMIT, + environment.ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS, + ); await migrateD1( environment.ARTIFACT_SERVER_D1_DATABASE, environment.ARTIFACT_SERVER_INSTALLATION_ID, @@ -158,7 +169,6 @@ async function createCloudflareRuntime( environment.ARTIFACT_SERVER_INSTALLATION_ID, ); const gitHistory = await initializeGitHistory(environment, repository, blobs); - const oidcIdentityProvider = oidcAuthentication(environment); const hostedAuthentication = await workOsAuthentication(environment); const browserAccess = hostedAuthentication !== null ? privateTeamBrowserAccess(browserLoginKinds.workOs) @@ -189,6 +199,11 @@ async function createCloudflareRuntime( repository, staging, }; + if (admissionPolicy !== null) { + Object.assign(applicationAdapters, { + externalIdentityAdmissionPolicy: admissionPolicy, + }); + } if (gitHistory.provider !== null) { Object.assign(applicationAdapters, {gitHistoryProvider: gitHistory.provider}); } @@ -347,6 +362,8 @@ function oidcValues( environment: WorkerEnvironment, ): readonly (string | undefined)[] { return [ + environment.ARTIFACT_SERVER_OIDC_AUTO_ADMIT, + environment.ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS, environment.ARTIFACT_SERVER_OIDC_CLIENT_ID, environment.ARTIFACT_SERVER_OIDC_CLIENT_SECRET, environment.ARTIFACT_SERVER_OIDC_ISSUER, diff --git a/deploy/cloudflare/tests/oidc-login-runtime.test.ts b/deploy/cloudflare/tests/oidc-login-runtime.test.ts index a90a4654..c5b0e30d 100644 --- a/deploy/cloudflare/tests/oidc-login-runtime.test.ts +++ b/deploy/cloudflare/tests/oidc-login-runtime.test.ts @@ -4,6 +4,7 @@ import {tmpdir} from "node:os"; import {join} from "node:path"; import {z} from "zod"; +import {exportJWK, generateKeyPair, SignJWT, type JWK} from "jose"; import {unstable_dev, type Unstable_DevWorker} from "wrangler"; import {afterAll, beforeAll, describe, expect, it} from "vitest"; @@ -22,9 +23,18 @@ const notReadySchema = z.object({ let issuerOrigin: string; let issuer: Server; +let signingKey: CryptoKey; +const issuedTokens = new Map(); beforeAll(async () => { - issuer = await startDiscoveryStub(); + const signing = await generateKeyPair("ES256"); + signingKey = signing.privateKey; + issuer = await startDiscoveryStub({ + ...await exportJWK(signing.publicKey), + alg: "ES256", + kid: "worker-oidc-test", + use: "sig", + }); const address = assignedAddressSchema.parse(issuer.address()); issuerOrigin = `http://127.0.0.1:${address.port}/cdn-cgi/access/sso/oidc/${clientId}`; @@ -37,6 +47,42 @@ afterAll(async () => { }); describe("Cloudflare Worker browser-login provider", () => { + it("AUTH-029: configured OIDC policy admits a new Worker member and rejects an unlisted domain", async () => { + const directory = await mkdtemp(join(tmpdir(), "artifact-server-oidc-admission-")); + const worker = await startWorker(directory, { + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "true", + ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: " EXAMPLE.TEST ", + ARTIFACT_SERVER_OIDC_CLIENT_ID: clientId, + ARTIFACT_SERVER_OIDC_ISSUER: `${issuerOrigin}/`, + }); + try { + expect((await completeWorkerLogin( + worker, "administrator@example.test", "administrator", + )).status).toBe(303); + const admitted = await completeWorkerLogin( + worker, "new-member@example.test", "new-member", + ); + expect(admitted.status).toBe(303); + const sessionCookie = admitted.headers.getSetCookie() + .find((cookie) => cookie.startsWith("__Host-artifact_session=")) + ?.split(";")[0]; + if (sessionCookie === undefined) throw new Error("Missing admitted session."); + const session = await worker.fetch(`${origin}/api/v1/session`, { + headers: {Cookie: sessionCookie}, + }); + expect(session.status).toBe(200); + expect(await session.json()).toMatchObject({ + principal: {kind: "human", membershipRole: "member"}, + }); + expect((await completeWorkerLogin( + worker, "unlisted@attacker.test", "unlisted", + )).status).toBe(403); + } finally { + await worker.stop(); + await rm(directory, {force: true, recursive: true}); + } + }, 120_000); + it("starts generic OIDC login from a path-based issuer", async () => { const directory = await mkdtemp(join(tmpdir(), "artifact-server-oidc-")); const worker = await startWorker(directory, { @@ -106,6 +152,41 @@ describe("Cloudflare Worker browser-login provider", () => { } }, 120_000); + it.each([ + {ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "yes"}, + {ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: "*.example.test"}, + ])("AUTH-029: refuses invalid OIDC admission configuration %j", async (settings) => { + const directory = await mkdtemp(join(tmpdir(), "artifact-server-oidc-policy-")); + const worker = await startWorker(directory, { + ARTIFACT_SERVER_OIDC_CLIENT_ID: clientId, + ARTIFACT_SERVER_OIDC_ISSUER: issuerOrigin, + ...settings, + }); + try { + const ready = await worker.fetch(`${origin}/ready`); + expect(ready.status).toBe(503); + expect(notReadySchema.parse(await ready.json()).error) + .toBe("artifact_server_not_ready"); + } finally { + await worker.stop(); + await rm(directory, {force: true, recursive: true}); + } + }, 120_000); + + it("AUTH-029: policy settings alone require an OIDC provider", async () => { + const directory = await mkdtemp(join(tmpdir(), "artifact-server-oidc-policy-alone-")); + const worker = await startWorker(directory, { + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "false", + }); + try { + const ready = await worker.fetch(`${origin}/ready`); + expect(ready.status).toBe(503); + } finally { + await worker.stop(); + await rm(directory, {force: true, recursive: true}); + } + }, 120_000); + it("fails private-team runtime initialization when no provider is configured", async () => { const directory = await mkdtemp(join(tmpdir(), "artifact-server-oidc-none-")); const worker = await startWorker(directory, {}); @@ -123,9 +204,62 @@ describe("Cloudflare Worker browser-login provider", () => { }, 120_000); }); -function startDiscoveryStub(): Promise { +async function completeWorkerLogin( + worker: Unstable_DevWorker, + email: string, + subject: string, +): Promise>> { + const login = await worker.fetch(`${origin}/auth/login`, {redirect: "manual"}); + expect(login.status).toBe(302); + const authorization = new URL(login.headers.get("location") ?? ""); + const nonce = authorization.searchParams.get("nonce"); + const state = authorization.searchParams.get("state"); + if (nonce === null || state === null) throw new Error("Missing OIDC handshake."); + const token = await new SignJWT({email, email_verified: true, nonce}) + .setProtectedHeader({alg: "ES256", kid: "worker-oidc-test"}) + .setIssuer(issuerOrigin) + .setAudience(clientId) + .setSubject(subject) + .setIssuedAt() + .setExpirationTime("5m") + .sign(signingKey); + issuedTokens.set(nonce, token); + const callback = new URL("/auth/callback", origin); + callback.searchParams.set("code", nonce); + callback.searchParams.set("state", state); + const cookie = login.headers.getSetCookie() + .map((entry) => entry.split(";")[0]).join("; "); + return worker.fetch(callback.toString(), { + headers: {Cookie: cookie}, + redirect: "manual", + }); +} + +function startDiscoveryStub(publicJwk: JWK): Promise { const server = createServer((request, response) => { const issuerPath = `/cdn-cgi/access/sso/oidc/${clientId}`; + if (request.url === `${issuerPath}/jwks`) { + response.writeHead(200, {"content-type": "application/json"}); + response.end(JSON.stringify({keys: [publicJwk]})); + return; + } + if (request.url === `${issuerPath}/token` && request.method === "POST") { + let body = ""; + request.on("data", (chunk: Buffer) => { + body += new TextDecoder().decode(chunk); + }); + request.on("end", () => { + const code = new URLSearchParams(body).get("code") ?? ""; + const token = issuedTokens.get(code); + issuedTokens.delete(code); + response.writeHead(token === undefined ? 400 : 200, { + "content-type": "application/json", + }); + response.end(JSON.stringify(token === undefined + ? {error: "invalid_grant"} : {id_token: token})); + }); + return; + } if (request.url !== `${issuerPath}/.well-known/openid-configuration`) { response.writeHead(404).end(); return; diff --git a/docs/deployment.md b/docs/deployment.md index 873fcbdb..1ebb8d86 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -20,7 +20,7 @@ Each remote deployment needs these boundaries: 1. Configure one HTTPS application origin. 2. Configure a separate wildcard content domain. 3. Configure WorkOS or one generic OIDC provider. -4. Admit team members through Artifact Server. +4. Admit team members through Artifact Server, or enable OIDC automatic admission. 5. Store service credentials outside source control. 6. Back up the database and artifact files. 7. Pin release deployments to an immutable image digest. @@ -39,6 +39,32 @@ Local-owner access works only on an exact loopback origin. Do not use it for rem Remote deployments use WorkOS or a generic OIDC provider. Network access and application authorization remain separate controls. +### Admit members through OIDC browser login + +By default, a successful identity-provider login still requires an active Artifact +Server membership. For an issuer that already restricts who may sign in, such as +Dex with a corporate-domain filter, set `ARTIFACT_SERVER_OIDC_AUTO_ADMIT=true` to +admit new browser users as normal members after OIDC token validation succeeds. +The switch accepts exactly `true` or `false` and defaults to `false`. + +Leave `ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS` unset or empty to trust the +configured issuer's access restrictions. To restrict new registrations further, +set a comma-separated list such as `example.com,example.org`. Domains are trimmed +and lowercased, then matched against the complete email domain: `example.com` +does not accept `sub.example.com`, `otherexample.com`, or wildcard entries. + +The domain list applies only to new automatic registrations. Explicitly admitted +members keep their roles and can sign in outside that list. A deactivated member +cannot regain access by changing the email attached to the same issuer and subject. +The first member still must be the configured bootstrap administrator; automatic +admission does not replace that bootstrap step or grant administrator privileges. +An explicitly unverified email remains denied. WorkOS, local-owner login, and MCP +bearer authentication keep their existing admission rules. + +Both policy variables require a complete OIDC configuration, even when automatic +admission is disabled. Invalid switch values or malformed domain lists fail startup. +For Helm, use `identity.oidcAutoAdmit` and `identity.oidcAllowedEmailDomains`. + ### Use a generic OIDC issuer for MCP The issuer configured for browser login also protects `/mcp`. Agents present an diff --git a/packaging/helm/artifact-server/README.md b/packaging/helm/artifact-server/README.md index db6b055d..35edec40 100644 --- a/packaging/helm/artifact-server/README.md +++ b/packaging/helm/artifact-server/README.md @@ -135,6 +135,34 @@ the chart mounts it as a file. `identity.oidcScopes` overrides the default `openid email profile`. The chart rejects a partial OIDC configuration, and an OIDC client secret or scope list without an issuer and client. +Browser login requires an active membership by default. To let the configured +issuer admit new users as normal members after token validation, enable +`identity.oidcAutoAdmit` (default `false`). Leave +`identity.oidcAllowedEmailDomains` at its default empty list to trust the issuer's +access restrictions, or provide exact email domains for new registrations: + +```yaml +identity: + oidcIssuer: https://idp.example.com/realms/main + oidcClientId: artifact-server + oidcAutoAdmit: true + oidcAllowedEmailDomains: + - example.com + - example.org +``` + +Domains are matched case-insensitively against the complete email domain; wildcards +and implicit subdomain matches are unsupported. The chart rejects enabled +automatic admission or a nonempty domain list without an OIDC issuer and client. +The rendered variables are `ARTIFACT_SERVER_OIDC_AUTO_ADMIT` and +`ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS`. + +Bootstrap the configured first administrator before other users sign in. Existing +members keep their roles and may sign in outside the registration domain list; +deactivated identities stay denied even when their provider email changes. +Automatic admission applies only to OIDC browser login and grants no administrator +role. MCP bearer authentication and WorkOS admission keep their existing rules. + The same issuer also protects the MCP endpoint: agents may present an end-user access token instead of an API key, and the server binds each call to the person who obtained it. Such a token must name `configuration.applicationOrigin` diff --git a/packaging/helm/artifact-server/templates/_helpers.tpl b/packaging/helm/artifact-server/templates/_helpers.tpl index bb50b641..4b0dd3bb 100644 --- a/packaging/helm/artifact-server/templates/_helpers.tpl +++ b/packaging/helm/artifact-server/templates/_helpers.tpl @@ -74,6 +74,9 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{- if and (not $hasOidcIssuer) (or (ne .Values.identity.oidcScopes "") (ne .Values.secret.keys.oidcClientSecret "")) -}} {{- fail "identity.oidcScopes and secret.keys.oidcClientSecret require identity.oidcClientId and identity.oidcIssuer" -}} {{- end -}} +{{- if and (not $hasOidcIssuer) (or .Values.identity.oidcAutoAdmit (gt (len .Values.identity.oidcAllowedEmailDomains) 0)) -}} +{{- fail "identity.oidcAutoAdmit and identity.oidcAllowedEmailDomains require identity.oidcClientId and identity.oidcIssuer" -}} +{{- end -}} {{- if and $hasOidcIssuer $hasWorkosIssuer -}} {{- fail "one installation has one browser-login provider: configure the identity.workos values or the identity.oidc values, not both" -}} {{- end -}} @@ -186,6 +189,10 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} value: /run/secrets/artifact-server/s3-secret-access-key {{- end }} {{- if .Values.identity.oidcIssuer }} +- name: ARTIFACT_SERVER_OIDC_AUTO_ADMIT + value: {{ .Values.identity.oidcAutoAdmit | quote }} +- name: ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS + value: {{ join "," .Values.identity.oidcAllowedEmailDomains | quote }} {{- if .Values.secret.keys.oidcClientSecret }} - name: ARTIFACT_SERVER_OIDC_CLIENT_SECRET_FILE value: /run/secrets/artifact-server/oidc-client-secret diff --git a/packaging/helm/artifact-server/values.schema.json b/packaging/helm/artifact-server/values.schema.json index 7f459499..a01d29e1 100644 --- a/packaging/helm/artifact-server/values.schema.json +++ b/packaging/helm/artifact-server/values.schema.json @@ -126,13 +126,22 @@ "identity": { "type": "object", "additionalProperties": false, - "required": ["workosClientId", "workosIssuer", "oidcClientId", "oidcIssuer", "oidcScopes"], + "required": ["workosClientId", "workosIssuer", "oidcClientId", "oidcIssuer", "oidcScopes", "oidcAutoAdmit", "oidcAllowedEmailDomains"], "properties": { "workosClientId": {"type": "string"}, "workosIssuer": {"type": "string"}, "oidcClientId": {"type": "string"}, "oidcIssuer": {"type": "string"}, - "oidcScopes": {"type": "string"} + "oidcScopes": {"type": "string"}, + "oidcAutoAdmit": {"type": "boolean"}, + "oidcAllowedEmailDomains": { + "type": "array", + "items": { + "type": "string", + "maxLength": 253, + "pattern": "^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + } + } } }, "serviceAccount": { diff --git a/packaging/helm/artifact-server/values.yaml b/packaging/helm/artifact-server/values.yaml index 258c6f86..a35ce8dd 100644 --- a/packaging/helm/artifact-server/values.yaml +++ b/packaging/helm/artifact-server/values.yaml @@ -44,6 +44,8 @@ identity: oidcClientId: "" oidcIssuer: "" oidcScopes: "" + oidcAutoAdmit: false + oidcAllowedEmailDomains: [] serviceAccount: create: true diff --git a/project/evidence/local-foundation.json b/project/evidence/local-foundation.json index 5c7d6c5d..2162614f 100644 --- a/project/evidence/local-foundation.json +++ b/project/evidence/local-foundation.json @@ -1 +1 @@ -{"numTotalTestSuites":204,"numPassedTestSuites":204,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":312,"numPassedTests":312,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1788451686755,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication CLI-003-B CLI-003-F: resumes a lost commit response across CLI process restarts","status":"passed","title":"CLI-003-B CLI-003-F: resumes a lost commit response across CLI process restarts","duration":4751.899541000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication CLI-002-B PUB-012-B: stores a verified profile outside the project and publishes through it after restart","status":"passed","title":"CLI-002-B PUB-012-B: stores a verified profile outside the project and publishes through it after restart","duration":12647.913417,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication CLI-002-F PUB-012-F: rejects an invalid credential without saving it and never sends one profile to another origin","status":"passed","title":"CLI-002-F PUB-012-F: rejects an invalid credential without saving it and never sends one profile to another origin","duration":4549.455624999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication completes browser PKCE, refresh, status, and revocation against real HTTP boundaries","status":"passed","title":"completes browser PKCE, refresh, status, and revocation against real HTTP boundaries","duration":3483.6122499999983,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451686977,"endTime":1788451712410.6123,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/cli/auth-profile-remote-publish.test.ts"},{"assertionResults":[{"ancestorTitles":["Artifact Server lifecycle CLI"],"fullName":"Artifact Server lifecycle CLI foundation: initialization, configuration, and support diagnostics are stable and secret-free","status":"passed","title":"foundation: initialization, configuration, and support diagnostics are stable and secret-free","duration":9228.416374999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Artifact Server lifecycle CLI"],"fullName":"Artifact Server lifecycle CLI foundation: compact serving withdraws readiness and integrity scans real committed bytes","status":"passed","title":"foundation: compact serving withdraws readiness and integrity scans real committed bytes","duration":6722.495875000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687440,"endTime":1788451703391.4958,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/cli/lifecycle-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link links a file named by a relative path and prints the binding","status":"passed","title":"links a file named by a relative path and prints the binding","duration":1314.725875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link explains the unavailable capability in one line and exits non-zero","status":"passed","title":"explains the unavailable capability in one line and exits non-zero","duration":1001.9761669999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link refuses a path outside the configured link roots","status":"passed","title":"refuses a path outside the configured link roots","duration":1100.5715000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link CLI-SEC-001-F: never sends a local-owner token to an arbitrary explicit origin","status":"passed","title":"CLI-SEC-001-F: never sends a local-owner token to an arbitrary explicit origin","duration":1166.5481250000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link CLI-SEC-002-F: refuses authenticated redirects","status":"passed","title":"CLI-SEC-002-F: refuses authenticated redirects","duration":1300.8540409999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link foundation: missing local credentials name real recovery commands","status":"passed","title":"foundation: missing local credentials name real recovery commands","duration":1217.7540840000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687660,"endTime":1788451694762.7542,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/cli/link-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI opens the managed application at a credential-free URL","status":"passed","title":"opens the managed application at a credential-free URL","duration":3726.513875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI starts with private reusable credentials without printing them","status":"passed","title":"starts with private reusable credentials without printing them","duration":2522.6100420000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI rejects an invalid port before creating persistent state","status":"passed","title":"rejects an invalid port before creating persistent state","duration":1201.6272920000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI PUB-002-B SCP-007-B: publishes a file, a large finished directory, and a second immutable version through real processes","status":"passed","title":"PUB-002-B SCP-007-B: publishes a file, a large finished directory, and a second immutable version through real processes","duration":10638.387416000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451686975,"endTime":1788451705064.3875,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/cli/local-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding stdio reuses one service and preserves modern discovery","status":"passed","title":"stdio reuses one service and preserves modern discovery","duration":8642.033667,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding connect and disconnect preserve unrelated client configuration","status":"passed","title":"connect and disconnect preserve unrelated client configuration","duration":658.3738750000011,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding foundation: native inspection failures stop before client mutation","status":"passed","title":"foundation: native inspection failures stop before client mutation","duration":154.5297499999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding MCP-020-B: resumes a journaled native-client change after process interruption","status":"passed","title":"MCP-020-B: resumes a journaled native-client change after process interruption","duration":514.8645419999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding MCP-020-F: rolls back failed mutation and preserves an independent interrupted change","status":"passed","title":"MCP-020-F: rolls back failed mutation and preserves an independent interrupted change","duration":585.0772500000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding refuses foreign and malformed JSON client configuration","status":"passed","title":"refuses foreign and malformed JSON client configuration","duration":13.204625000000306,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding ambiguous connect and doctor failures do not create or repair state","status":"passed","title":"ambiguous connect and doctor failures do not create or repair state","duration":5052.8505829999995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687162,"endTime":1788451702782.8506,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/cli/mcp-onboarding.test.ts"},{"assertionResults":[{"ancestorTitles":["artifactserver start with linked files enabled"],"fullName":"artifactserver start with linked files enabled the environment flags reach the direct-start server and linking works","status":"passed","title":"the environment flags reach the direct-start server and linking works","duration":1387.64525,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687660,"endTime":1788451689047.6453,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/cli/start-linked-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["bridge activity beacon"],"fullName":"bridge activity beacon beacons fire once each at bundle acceptance, first reply, and settled work","status":"passed","title":"beacons fire once each at bundle acceptance, first reply, and settled work","duration":4510.862166999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge activity beacon"],"fullName":"bridge activity beacon a failing beacon transport never disturbs delivery, replies, or the host","status":"passed","title":"a failing beacon transport never disturbs delivery, replies, or the host","duration":4177.068375,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687690,"endTime":1788451696378.0684,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/bridge-core-beacon.test.ts"},{"assertionResults":[{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization strips every bidirectional control while keeping the visible text","status":"passed","title":"strips every bidirectional control while keeping the visible text","duration":3.3274590000000046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization strips every zero-width and invisible character","status":"passed","title":"strips every zero-width and invisible character","duration":0.3095830000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization defuses a trojan-source style reordering attack in one pass","status":"passed","title":"defuses a trojan-source style reordering attack in one pass","duration":0.13716699999997672,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization leaves ordinary multilingual text, whitespace, and punctuation alone","status":"passed","title":"leaves ordinary multilingual text, whitespace, and punctuation alone","duration":0.05791700000000333,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization the rendered bundle strips hostile controls from body, note, and quoted selection","status":"passed","title":"the rendered bundle strips hostile controls from body, note, and quoted selection","duration":0.48816599999997834,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699607,"endTime":1788451699611.4883,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/bridge-core-sanitization.test.ts"},{"assertionResults":[{"ancestorTitles":["claude channel bridge"],"fullName":"claude channel bridge a dispatched bundle arrives as a channel notification and the tool closes the loop","status":"passed","title":"a dispatched bundle arrives as a channel notification and the tool closes the loop","duration":2799.905333,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687668,"endTime":1788451690467.9053,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/claude-channel.test.ts"},{"assertionResults":[{"ancestorTitles":["file publication client"],"fullName":"file publication client publishes an ordinary file, a complete directory, and an optimistic new version","status":"passed","title":"publishes an ordinary file, a complete directory, and an optimistic new version","duration":258.202584,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client classifies unsafe filesystem inputs before it creates an upload","status":"passed","title":"classifies unsafe filesystem inputs before it creates an upload","duration":6.574458999999933,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client fails closed for invalid credentials, invalid server origins, and cross-origin upload plans","status":"passed","title":"fails closed for invalid credentials, invalid server origins, and cross-origin upload plans","duration":10.19399999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client rejects incomplete and malformed plans and reports an unavailable server as protocol failures","status":"passed","title":"rejects incomplete and malformed plans and reports an unavailable server as protocol failures","duration":41.969875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client uses deterministic media types and a safe binary fallback","status":"passed","title":"uses deterministic media types and a safe binary fallback","duration":0.141208000000006,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695061,"endTime":1788451695378.141,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/file-publication-client.test.ts"},{"assertionResults":[{"ancestorTitles":["opencode bridge adapter"],"fullName":"opencode bridge adapter registers as kind opencode, holds delivery until a top-level session exists, injects through promptAsync, and beacons each work boundary","status":"passed","title":"registers as kind opencode, holds delivery until a top-level session exists, injects through promptAsync, and beacons each work boundary","duration":5334.985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["opencode bridge adapter"],"fullName":"opencode bridge adapter fails open when the plugin context lacks the expected surface: no registration, no crash, an honest tool error","status":"passed","title":"fails open when the plugin context lacks the expected surface: no registration, no crash, an honest tool error","duration":962.1376249999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["opencode bridge adapter"],"fullName":"opencode bridge adapter DSP-013-F: a session deleted during injection fails that dispatch, releases its comment, and the same bridge delivers the next dispatch","status":"passed","title":"DSP-013-F: a session deleted during injection fails that dispatch, releases its comment, and the same bridge delivers the next dispatch","duration":5277.451625000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687694,"endTime":1788451699268.4517,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/opencode-bridge.test.ts"},{"assertionResults":[{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks renders the recorded bundle template with note, paths, selections, and instruction","status":"passed","title":"renders the recorded bundle template with note, paths, selections, and instruction","duration":1.1153750000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks omits the note line when absent and bounds long selections to the cap","status":"passed","title":"omits the note line when absent and bounds long selections to the cap","duration":0.1640000000000157,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks hostile slash-leading fields can never produce a slash-leading message","status":"passed","title":"hostile slash-leading fields can never produce a slash-leading message","duration":0.094333000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks connection keys are stable sha-256 digests of host and directory","status":"passed","title":"connection keys are stable sha-256 digests of host and directory","duration":0.8905420000000106,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks the display name prefers the override and falls back to the basename","status":"passed","title":"the display name prefers the override and falls back to the basename","duration":0.14595800000000736,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks environment configuration wins over local discovery and normalizes the origin","status":"passed","title":"environment configuration wins over local discovery and normalizes the origin","duration":7.728041999999988,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks local discovery resolves the loopback service record and token","status":"passed","title":"local discovery resolves the loopback service record and token","duration":5.864292000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks no configuration, a non-loopback record, or a short token stays dormant","status":"passed","title":"no configuration, a non-loopback record, or a short token stays dormant","duration":9.768499999999989,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699469,"endTime":1788451699494.7686,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/pi-bridge-core.test.ts"},{"assertionResults":[{"ancestorTitles":["tool-result nudge composition"],"fullName":"tool-result nudge composition picks exactly one kind in priority order and nothing when nothing applies","status":"passed","title":"picks exactly one kind in priority order and nothing when nothing applies","duration":1.5139579999999881,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tool-result nudge composition"],"fullName":"tool-result nudge composition strips bidirectional and invisible characters from anything a nudge quotes","status":"passed","title":"strips bidirectional and invisible characters from anything a nudge quotes","duration":0.3152079999999984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tool-result nudge composition"],"fullName":"tool-result nudge composition bounds one block and appends it as a trailing text item only","status":"passed","title":"bounds one block and appends it as a trailing text item only","duration":0.734375,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699568,"endTime":1788451699570.7344,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/tool-result-nudges.test.ts"},{"assertionResults":[{"ancestorTitles":["WebMCP mutation echo counts"],"fullName":"WebMCP mutation echo counts resolving the last open thread counts it resolved before the view commits","status":"passed","title":"resolving the last open thread counts it resolved before the view commits","duration":1.0251669999999962,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["WebMCP mutation echo counts"],"fullName":"WebMCP mutation echo counts a thread the view has not loaded yet is counted from the fresh record","status":"passed","title":"a thread the view has not loaded yet is counted from the fresh record","duration":0.16566600000000165,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["WebMCP mutation echo counts"],"fullName":"WebMCP mutation echo counts an unchanged thread leaves the counts as the view has them","status":"passed","title":"an unchanged thread leaves the counts as the view has them","duration":0.15083300000000577,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699555,"endTime":1788451699557.151,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/client/webmcp-counts.test.ts"},{"assertionResults":[{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract normalizes the shared defaults and accepts each native provider shape","status":"passed","title":"normalizes the shared defaults and accepts each native provider shape","duration":9.055457999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract accepts isolated private DNS names without requiring a public suffix","status":"passed","title":"accepts isolated private DNS names without requiring a public suffix","duration":0.7179580000000101,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects floating image tag before a provider write","status":"passed","title":"rejects floating image tag before a provider write","duration":1.1681669999999826,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects same registrable content domain before a provider write","status":"passed","title":"rejects same registrable content domain before a provider write","duration":0.32241700000002993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects same private DNS boundary before a provider write","status":"passed","title":"rejects same private DNS boundary before a provider write","duration":0.36624999999997954,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects reversed capacity before a provider write","status":"passed","title":"rejects reversed capacity before a provider write","duration":0.18645900000001348,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects disabled production deletion protection before a provider write","status":"passed","title":"rejects disabled production deletion protection before a provider write","duration":0.5325420000000349,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects missing public DNS before a provider write","status":"passed","title":"rejects missing public DNS before a provider write","duration":0.42533400000002075,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects one zone for isolated domains before a provider write","status":"passed","title":"rejects one zone for isolated domains before a provider write","duration":0.2715000000000032,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects incomplete WorkOS configuration before a provider write","status":"passed","title":"rejects incomplete WorkOS configuration before a provider write","duration":0.5165000000000077,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects credential-bearing telemetry URL before a provider write","status":"passed","title":"rejects credential-bearing telemetry URL before a provider write","duration":0.4448330000000169,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects credential-bearing state backend before a provider write","status":"passed","title":"rejects credential-bearing state backend before a provider write","duration":0.32095800000001873,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects main stack state-backend creation before a provider write","status":"passed","title":"rejects main stack state-backend creation before a provider write","duration":0.23366699999996854,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects unsupported direct Azure target before a provider write","status":"passed","title":"rejects unsupported direct Azure target before a provider write","duration":0.13766700000002174,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects local Alchemy state outside development before a provider write","status":"passed","title":"rejects local Alchemy state outside development before a provider write","duration":0.2933330000000183,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects invalid Cloudflare compatibility date before a provider write","status":"passed","title":"rejects invalid Cloudflare compatibility date before a provider write","duration":0.27174999999999727,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects non-global Cloudflare region before a provider write","status":"passed","title":"rejects non-global Cloudflare region before a provider write","duration":0.3662909999999897,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract accepts exact secret-free outputs and binds URLs to the requested stack","status":"passed","title":"accepts exact secret-free outputs and binds URLs to the requested stack","duration":0.9531670000000076,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects output drift, extra output keys, credential URLs, and known secrets","status":"passed","title":"rejects output drift, extra output keys, credential URLs, and known secrets","duration":1.6345000000000027,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract keeps partial evidence for diagnosis but requires every passing gate for release","status":"passed","title":"keeps partial evidence for diagnosis but requires every passing gate for release","duration":1.771665999999982,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects evidence for the wrong tool, impossible timestamps, and credential locations","status":"passed","title":"rejects evidence for the wrong tool, impossible timestamps, and credential locations","duration":0.8300830000000019,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects duplicate release checks and unknown evidence fields","status":"passed","title":"rejects duplicate release checks and unknown evidence fields","duration":0.5535830000000033,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699664,"endTime":1788451699685.5535,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/deployment/cloud-deployment-contract.test.ts"},{"assertionResults":[{"ancestorTitles":["download filenames"],"fullName":"download filenames unsafe header characters cannot escape the attachment filename","status":"passed","title":"unsafe header characters cannot escape the attachment filename","duration":1.264916999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["download filenames"],"fullName":"download filenames an unusable filename gets a stable portable fallback","status":"passed","title":"an unusable filename gets a stable portable fallback","duration":0.22758299999999565,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699603,"endTime":1788451699604.265,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/http/content-disposition.test.ts"},{"assertionResults":[{"ancestorTitles":["linked artifacts over the local HTTP boundary"],"fullName":"linked artifacts over the local HTTP boundary a file links in place, drifts, and captures a new version","status":"passed","title":"a file links in place, drifts, and captures a new version","duration":270.972792,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked artifacts over the local HTTP boundary"],"fullName":"linked artifacts over the local HTTP boundary every linked route answers the stable shape while the capability is off","status":"passed","title":"every linked route answers the stable shape while the capability is off","duration":114.22133300000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked artifacts over the local HTTP boundary"],"fullName":"linked artifacts over the local HTTP boundary the live origin streams current disk bytes to a session holder only","status":"passed","title":"the live origin streams current disk bytes to a session holder only","duration":329.8208330000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697292,"endTime":1788451698007.8208,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/http/linked-artifacts.test.ts"},{"assertionResults":[{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: large JSON API responses are gzip-compressed when accepted","status":"passed","title":"foundation: large JSON API responses are gzip-compressed when accepted","duration":892.69825,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: static assets compress, revalidate with 304, and keep immutable caching","status":"passed","title":"foundation: static assets compress, revalidate with 304, and keep immutable caching","duration":95.7316659999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: brotli is preferred over gzip when the client allows both","status":"passed","title":"foundation: brotli is preferred over gzip when the client allows both","duration":61.55733400000008,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: the application shell compresses without losing revalidation","status":"passed","title":"foundation: the application shell compresses without losing revalidation","duration":44.432000000000016,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: small and binary responses stay identity-encoded","status":"passed","title":"foundation: small and binary responses stay identity-encoded","duration":30.40587500000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: range-capable content responses are never compressed","status":"passed","title":"foundation: range-capable content responses are never compressed","duration":59.475541999999905,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693551,"endTime":1788451694735.4756,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/http/node-response-compression.test.ts"},{"assertionResults":[{"ancestorTitles":["Node web asset store"],"fullName":"Node web asset store foundation: assets are read and digested once per process","status":"passed","title":"foundation: assets are read and digested once per process","duration":21.17133299999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node web asset store"],"fullName":"Node web asset store foundation: HEAD serves headers without a body and misses are not cached","status":"passed","title":"foundation: HEAD serves headers without a body and misses are not cached","duration":35.967416000000014,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699172,"endTime":1788451699229.9675,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/http/node-web-asset-store.test.ts"},{"assertionResults":[{"ancestorTitles":["graceful HTTP shutdown"],"fullName":"graceful HTTP shutdown withdraws readiness and preserves accepted work before closing resources","status":"passed","title":"withdraws readiness and preserves accepted work before closing resources","duration":19.452958999999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["graceful HTTP shutdown"],"fullName":"graceful HTTP shutdown terminates stuck work at the configured deadline before closing resources","status":"passed","title":"terminates stuck work at the configured deadline before closing resources","duration":28.454916999999995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699408,"endTime":1788451699456.4548,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/lifecycle/graceful-http-shutdown.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact catalog comment queries"],"fullName":"artifact catalog comment queries ART-011-B ART-011-F: comment filters and ordering run before keyset pagination","status":"passed","title":"ART-011-B ART-011-F: comment filters and ordering run before keyset pagination","duration":362.8835,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699057,"endTime":1788451699419.8835,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/artifact-catalog-comments.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact comparisons"],"fullName":"artifact comparisons AUTH-004-B DIF-001-B DIF-002-B DIF-002-F DIF-003-B DIF-003-F DIF-004-B DIF-004-F DIF-005-B DIF-005-F: public content exposes no history while authenticated comparisons work without Git","status":"passed","title":"AUTH-004-B DIF-001-B DIF-002-B DIF-002-F DIF-003-B DIF-003-F DIF-004-B DIF-004-F DIF-005-B DIF-005-F: public content exposes no history while authenticated comparisons work without Git","duration":831.8990000000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696020,"endTime":1788451696851.899,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/artifact-comparison.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact lifecycle"],"fullName":"artifact lifecycle foundation: artifact and action lists use bounded stable cursors","status":"passed","title":"foundation: artifact and action lists use bounded stable cursors","duration":338.86004199999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifact lifecycle"],"fullName":"artifact lifecycle ART-006-B ART-006-F ART-007-B: deletion tombstones the artifact, blocks every read path, and retains committed versions","status":"passed","title":"ART-006-B ART-006-F ART-007-B: deletion tombstones the artifact, blocks every read path, and retains committed versions","duration":212.36516600000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifact lifecycle"],"fullName":"artifact lifecycle foundation: a tombstone cannot commit without its action record","status":"passed","title":"foundation: a tombstone cannot commit without its action record","duration":107.33995900000014,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451692630,"endTime":1788451693288.3398,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/artifact-lifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact and version management"],"fullName":"artifact and version management ART-002-B ART-003-B ART-004-B VER-001-B VER-001-F: authenticated metadata exposes immutable history and exact earlier bytes","status":"passed","title":"ART-002-B ART-003-B ART-004-B VER-001-B VER-001-F: authenticated metadata exposes immutable history and exact earlier bytes","duration":212.085042,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifact and version management"],"fullName":"artifact and version management ART-005-B ART-005-F: restore and visibility changes are atomic, attributed, and fail closed","status":"passed","title":"ART-005-B ART-005-F: restore and visibility changes are atomic, attributed, and fail closed","duration":224.58020899999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451690900,"endTime":1788451691336.5803,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/artifact-management.test.ts"},{"assertionResults":[{"ancestorTitles":["project-wide artifact search"],"fullName":"project-wide artifact search ART-009-B ART-009-F: search matches normalized name substrings and exact tags before pagination","status":"passed","title":"ART-009-B ART-009-F: search matches normalized name substrings and exact tags before pagination","duration":332.53187500000007,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695004,"endTime":1788451695336.532,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/artifact-search.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact tags"],"fullName":"artifact tags ART-008-B ART-008-F: tags normalize, filter exactly, persist with the artifact, and reject invalid sets atomically","status":"passed","title":"ART-008-B ART-008-F: tags normalize, filter exactly, persist with the artifact, and reject invalid sets atomically","duration":372.299458,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698596,"endTime":1788451698968.2996,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/artifact-tags.test.ts"},{"assertionResults":[{"ancestorTitles":["managed API-key rotation"],"fullName":"managed API-key rotation rotation preserves human and service attribution while replacing each credential","status":"passed","title":"rotation preserves human and service attribution while replacing each credential","duration":312.97849999999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699012,"endTime":1788451699324.9785,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/auth-009-api-key-rotation.test.ts"},{"assertionResults":[{"ancestorTitles":["generic OIDC browser login"],"fullName":"generic OIDC browser login AUTH-019-B: a real OIDC round trip admits the bootstrap administrator and reuses the issuer-qualified binding","status":"passed","title":"AUTH-019-B: a real OIDC round trip admits the bootstrap administrator and reuses the issuer-qualified binding","duration":105.01383299999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC browser login"],"fullName":"generic OIDC browser login AUTH-019-F: unadmitted, disavowed, and email-less OIDC identities are refused without members or sessions","status":"passed","title":"AUTH-019-F: unadmitted, disavowed, and email-less OIDC identities are refused without members or sessions","duration":39.39799999999991,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699578,"endTime":1788451699722.398,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/auth-019-oidc-login.test.ts"},{"assertionResults":[{"ancestorTitles":["OIDC id_token validation"],"fullName":"OIDC id_token validation AUTH-020-B: id_tokens skewed inside the thirty-second tolerance still complete login","status":"passed","title":"AUTH-020-B: id_tokens skewed inside the thirty-second tolerance still complete login","duration":126.22779100000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC id_token validation"],"fullName":"OIDC id_token validation AUTH-020-F: every token, state, and attempt defect is refused without a session, a member, or a leaked credential","status":"passed","title":"AUTH-020-F: every token, state, and attempt defect is refused without a session, a member, or a leaked credential","duration":314.96570900000006,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695948,"endTime":1788451696388.9658,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/auth-020-oidc-validation.test.ts"},{"assertionResults":[{"ancestorTitles":["generic OIDC configuration"],"fullName":"generic OIDC configuration AUTH-021-B: a complete OIDC configuration serves browser login and no configured provider keeps keys working","status":"passed","title":"AUTH-021-B: a complete OIDC configuration serves browser login and no configured provider keeps keys working","duration":1060.181125,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC configuration"],"fullName":"generic OIDC configuration AUTH-021-F: partial, doubled, and malformed OIDC configuration fails startup or login without weakening the server","status":"passed","title":"AUTH-021-F: partial, doubled, and malformed OIDC configuration fails startup or login without weakening the server","duration":3515.075417,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451688877,"endTime":1788451693452.0754,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/auth-021-oidc-configuration.test.ts"},{"assertionResults":[{"ancestorTitles":["authorization policy"],"fullName":"authorization policy AUTH-008-B AUTH-008-F: standalone mutations use membership and explicit capabilities","status":"passed","title":"AUTH-008-B AUTH-008-F: standalone mutations use membership and explicit capabilities","duration":6.7470830000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authorization policy"],"fullName":"authorization policy foundation: another installation or an unscoped service cannot acquire authority","status":"passed","title":"foundation: another installation or an unscoped service cannot acquire authority","duration":0.4507089999999607,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authorization policy"],"fullName":"authorization policy project discovery follows membership and explicit service authority","status":"passed","title":"project discovery follows membership and explicit service authority","duration":0.8367920000000026,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699687,"endTime":1788451699694.837,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/authorization-policy.test.ts"},{"assertionResults":[{"ancestorTitles":["local immutable blob storage"],"fullName":"local immutable blob storage ART-004-F PUB-004-F: a conflicting stream cannot replace an existing fingerprint","status":"passed","title":"ART-004-F PUB-004-F: a conflicting stream cannot replace an existing fingerprint","duration":66.528167,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699222,"endTime":1788451699288.528,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/blob-storage.test.ts"},{"assertionResults":[{"ancestorTitles":["extracted bridge core"],"fullName":"extracted bridge core BRP-001-B: the extracted core drives a fake host through register, claim, deliver, reply, and resolve against a real server","status":"passed","title":"BRP-001-B: the extracted core drives a fake host through register, claim, deliver, reply, and resolve against a real server","duration":4417.548417,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["extracted bridge core"],"fullName":"extracted bridge core BRP-001-F: the bridge core module graph reaches no Pi module and only host-neutral dependencies","status":"passed","title":"BRP-001-F: the bridge core module graph reaches no Pi module and only host-neutral dependencies","duration":70.51495800000066,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687689,"endTime":1788451692177.515,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/brp-001-extracted-bridge-core.test.ts"},{"assertionResults":[{"ancestorTitles":["mailbox-tier dispatch inbox over MCP"],"fullName":"mailbox-tier dispatch inbox over MCP BRP-002-B: an MCP principal lists its inbox with implicit mailbox registration, claims the oldest dispatch as a rendered sanitized bundle, and reports delivered, and the agent listing shows the mailbox tier","status":"passed","title":"BRP-002-B: an MCP principal lists its inbox with implicit mailbox registration, claims the oldest dispatch as a rendered sanitized bundle, and reports delivered, and the agent listing shows the mailbox tier","duration":562.2027909999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["mailbox-tier dispatch inbox over MCP"],"fullName":"mailbox-tier dispatch inbox over MCP BRP-002-F: a second principal never sees the first principal's dispatches, claim with an empty inbox answers cleanly, and a delivered report for a dispatch the caller does not hold is refused","status":"passed","title":"BRP-002-F: a second principal never sees the first principal's dispatches, claim with an empty inbox answers cleanly, and a delivered report for a dispatch the caller does not hold is refused","duration":264.14750000000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697872,"endTime":1788451698698.1475,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/brp-002-mailbox-inbox.test.ts"},{"assertionResults":[{"ancestorTitles":["comment threads pin one exact saved version"],"fullName":"comment threads pin one exact saved version CMT-001-B: a thread keeps naming its original artifact and version across later publishes and a restore","status":"passed","title":"CMT-001-B: a thread keeps naming its original artifact and version across later publishes and a restore","duration":479.356375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment threads pin one exact saved version"],"fullName":"comment threads pin one exact saved version CMT-001-F: publishing and restoring change nothing on an existing thread and a thread on an unknown version is refused","status":"passed","title":"CMT-001-F: publishing and restoring change nothing on an existing thread and a thread on an unknown version is refused","duration":215.70170800000005,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693891,"endTime":1788451694585.7017,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-001-exact-version-thread.test.ts"},{"assertionResults":[{"ancestorTitles":["comment paths are validated against the version manifest"],"fullName":"comment paths are validated against the version manifest CMT-002-B: a thread names one manifest entry path inside its version and reads that path back","status":"passed","title":"CMT-002-B: a thread names one manifest entry path inside its version and reads that path back","duration":156.88816700000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment paths are validated against the version manifest"],"fullName":"comment paths are validated against the version manifest CMT-002-F: a path outside that version's manifest is refused as an invalid comment and stores nothing","status":"passed","title":"CMT-002-F: a path outside that version's manifest is refused as an invalid comment and stores nothing","duration":179.20725000000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451691805,"endTime":1788451692141.2073,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-002-manifest-path.test.ts"},{"assertionResults":[{"ancestorTitles":["comment anchors stay opaque within their documented bounds"],"fullName":"comment anchors stay opaque within their documented bounds CMT-003-B: anchors of several shapes are stored and read back byte for byte","status":"passed","title":"CMT-003-B: anchors of several shapes are stored and read back byte for byte","duration":276.89637500000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment anchors stay opaque within their documented bounds"],"fullName":"comment anchors stay opaque within their documented bounds CMT-003-F: oversize anchors and out-of-range points are refused while a valid anchor stays unchanged","status":"passed","title":"CMT-003-F: oversize anchors and out-of-range points are refused while a valid anchor stays unchanged","duration":158.78641700000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698523,"endTime":1788451698958.7864,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-003-anchor-opacity.test.ts"},{"assertionResults":[{"ancestorTitles":["comment replies are one level deep and open-thread only"],"fullName":"comment replies are one level deep and open-thread only CMT-004-B: an open thread collects replies that carry no replies of their own","status":"passed","title":"CMT-004-B: an open thread collects replies that carry no replies of their own","duration":139.20220900000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment replies are one level deep and open-thread only"],"fullName":"comment replies are one level deep and open-thread only CMT-004-F: a resolved thread refuses replies and a concurrent resolve never leaves a reply on a resolved thread","status":"passed","title":"CMT-004-F: a resolved thread refuses replies and a concurrent resolve never leaves a reply on a resolved thread","duration":148.300292,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451691278,"endTime":1788451691566.3003,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-004-reply-rules.test.ts"},{"assertionResults":[{"ancestorTitles":["resolving and reopening one comment thread"],"fullName":"resolving and reopening one comment thread CMT-005-B: one state change resolves and reopens a thread and records who changed it and when","status":"passed","title":"CMT-005-B: one state change resolves and reopens a thread and records who changed it and when","duration":193.947208,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["resolving and reopening one comment thread"],"fullName":"resolving and reopening one comment thread CMT-005-F: an unknown state value is refused and a resolved thread stays resolved on every read path","status":"passed","title":"CMT-005-F: an unknown state value is refused and a resolved thread stays resolved on every read path","duration":187.91712500000006,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695143,"endTime":1788451695524.9172,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-005-state-transitions.test.ts"},{"assertionResults":[{"ancestorTitles":["comment read authority"],"fullName":"comment read authority CMT-006-B: an admitted member reads threads and replies on the application origin","status":"passed","title":"CMT-006-B: an admitted member reads threads and replies on the application origin","duration":275.081708,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment read authority"],"fullName":"comment read authority CMT-006-F: public-link viewers, anonymous requests, and the content origin never see comments","status":"passed","title":"CMT-006-F: public-link viewers, anonymous requests, and the content origin never see comments","duration":126.76854200000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695170,"endTime":1788451695571.7686,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-006-comment-read-authority.test.ts"},{"assertionResults":[{"ancestorTitles":["comment write capability"],"fullName":"comment write capability CMT-007-B: a direct human member and a comment-write key open, answer, resolve, and reopen threads","status":"passed","title":"CMT-007-B: a direct human member and a comment-write key open, answer, resolve, and reopen threads","duration":431.743708,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment write capability"],"fullName":"comment write capability CMT-007-F: a key holding only artifact read authority is denied every comment mutation","status":"passed","title":"CMT-007-F: a key holding only artifact read authority is denied every comment mutation","duration":261.661333,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697430,"endTime":1788451698123.6614,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-007-comment-write-capability.test.ts"},{"assertionResults":[{"ancestorTitles":["comment authorship"],"fullName":"comment authorship CMT-008-B: authors edit and delete their own comments while an administrator deletes another member's thread","status":"passed","title":"CMT-008-B: authors edit and delete their own comments while an administrator deletes another member's thread","duration":243.36808400000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment authorship"],"fullName":"comment authorship CMT-008-F: another member and an administrator cannot rewrite an author's comment while the administrator's delete is recorded","status":"passed","title":"CMT-008-F: another member and an administrator cannot rewrite an author's comment while the administrator's delete is recorded","duration":160.82187499999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451692653,"endTime":1788451693056.8218,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-008-comment-authorship.test.ts"},{"assertionResults":[{"ancestorTitles":["comment project scope"],"fullName":"comment project scope CMT-009-B: every comment operation runs inside its own project and returns project-scoped records","status":"passed","title":"CMT-009-B: every comment operation runs inside its own project and returns project-scoped records","duration":291.451375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment project scope"],"fullName":"comment project scope CMT-009-F: identifiers presented under another project or installation are not found without disclosure","status":"passed","title":"CMT-009-F: identifiers presented under another project or installation are not found without disclosure","duration":321.3900839999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698464,"endTime":1788451699077.3901,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-009-comment-project-scope.test.ts"},{"assertionResults":[{"ancestorTitles":["comment create idempotency"],"fullName":"comment create idempotency CMT-010-B: repeated thread and reply creates with one key replay the original records","status":"passed","title":"CMT-010-B: repeated thread and reply creates with one key replay the original records","duration":222.79387500000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment create idempotency"],"fullName":"comment create idempotency CMT-010-F: one key with different input conflicts and writes no duplicate comment","status":"passed","title":"CMT-010-F: one key with different input conflicts and writes no duplicate comment","duration":185.32100000000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695091,"endTime":1788451695499.321,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-010-comment-idempotency.test.ts"},{"assertionResults":[{"ancestorTitles":["comment mutations and the action ledger"],"fullName":"comment mutations and the action ledger CMT-011-B: every comment mutation appends an attributed action naming the artifact and version","status":"passed","title":"CMT-011-B: every comment mutation appends an attributed action naming the artifact and version","duration":431.01124999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment mutations and the action ledger"],"fullName":"comment mutations and the action ledger CMT-011-F: a comment mutation cannot commit when its action record is refused","status":"passed","title":"CMT-011-F: a comment mutation cannot commit when its action record is refused","duration":159.47416699999985,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693913,"endTime":1788451694503.474,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-011-comment-action-ledger.test.ts"},{"assertionResults":[{"ancestorTitles":["comment surface parity and change polling"],"fullName":"comment surface parity and change polling CMT-012-B: HTTP and MCP drive the same comment operations and page changes with a since time and a cursor","status":"passed","title":"CMT-012-B: HTTP and MCP drive the same comment operations and page changes with a since time and a cursor","duration":462.155291,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment surface parity and change polling"],"fullName":"comment surface parity and change polling CMT-012-F: MCP and HTTP agree for one principal and a since poll resuming from the previous pass start misses no change","status":"passed","title":"CMT-012-F: MCP and HTTP agree for one principal and a since poll resuming from the previous pass start misses no change","duration":346.441292,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697465,"endTime":1788451698273.4414,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-012-comment-surface-parity.test.ts"},{"assertionResults":[{"ancestorTitles":["version file route for the review viewer"],"fullName":"version file route for the review viewer CMT-013-B: an artifact-read principal reads exact manifest bytes with non-renderable headers","status":"passed","title":"CMT-013-B: an artifact-read principal reads exact manifest bytes with non-renderable headers","duration":344.584709,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["version file route for the review viewer"],"fullName":"version file route for the review viewer foundation: version file responses revalidate and serve byte ranges","status":"passed","title":"foundation: version file responses revalidate and serve byte ranges","duration":324.4429170000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["version file route for the review viewer"],"fullName":"version file route for the review viewer CMT-013-F: unauthenticated, public-link, and read-incapable requests get no manifest bytes","status":"passed","title":"CMT-013-F: unauthenticated, public-link, and read-incapable requests get no manifest bytes","duration":200.08024999999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693718,"endTime":1788451694587.0803,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-013-version-file-route.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatched comment deletion"],"fullName":"dispatched comment deletion CMT-020-B: queued, claimed, and delivered dispatches hold comments while addressed, failed, and canceled dispatches release them","status":"passed","title":"CMT-020-B: queued, claimed, and delivered dispatches hold comments while addressed, failed, and canceled dispatches release them","duration":293.389209,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatched comment deletion"],"fullName":"dispatched comment deletion CMT-020-F: a concurrent send and delete commits only one complete SQLite outcome","status":"passed","title":"CMT-020-F: a concurrent send and delete commits only one complete SQLite outcome","duration":73.07408299999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695232,"endTime":1788451695599.074,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-020-dispatched-comment-deletion.test.ts"},{"assertionResults":[{"ancestorTitles":["complete immutable version downloads"],"fullName":"complete immutable version downloads CMT-021-B: a permitted user downloads the selected version as a deterministic path-preserving ZIP","status":"passed","title":"CMT-021-B: a permitted user downloads the selected version as a deterministic path-preserving ZIP","duration":267.00179199999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete immutable version downloads"],"fullName":"complete immutable version downloads CMT-021-F: unauthorized and mismatched version requests disclose no artifact bytes","status":"passed","title":"CMT-021-F: unauthorized and mismatched version requests disclose no artifact bytes","duration":394.286167,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697306,"endTime":1788451697967.2861,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/cmt-021-version-download.test.ts"},{"assertionResults":[{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing MAN-006-B CNT-005-B: a canonical complete site serves every declared file from one exact version origin","status":"passed","title":"MAN-006-B CNT-005-B: a canonical complete site serves every declared file from one exact version origin","duration":256.40245799999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing foundation: a configured public origin owns every application URL behind a TLS-terminating proxy","status":"passed","title":"foundation: a configured public origin owns every application URL behind a TLS-terminating proxy","duration":30.736125000000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing PUB-001-B PUB-003-B: a staged site resumes after restart and commits only after every file is verified","status":"passed","title":"PUB-001-B PUB-003-B: a staged site resumes after restart and commits only after every file is verified","duration":93.78554199999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing MAN-005-F: absent entries, duplicate paths, and portable-name collisions are rejected before upload creation","status":"passed","title":"MAN-005-F: absent entries, duplicate paths, and portable-name collisions are rejected before upload creation","duration":35.209082999999964,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing CNT-007-B CNT-007-F: SPA fallback is explicit, navigation-only, and never replaces an exact file","status":"passed","title":"CNT-007-B CNT-007-F: SPA fallback is explicit, navigation-only, and never replaces an exact file","duration":104.73858299999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing immutable files support one bounded byte range without buffering the full object","status":"passed","title":"immutable files support one bounded byte range without buffering the full object","duration":56.97945799999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing CNT-002-B CNT-002-F: ordinary files use browser-native or download-safe responses","status":"passed","title":"CNT-002-B CNT-002-F: ordinary files use browser-native or download-safe responses","duration":264.4285839999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing foundation: SPA routing rejects a non-HTML entry before allocating upload slots","status":"passed","title":"foundation: SPA routing rejects a non-HTML entry before allocating upload slots","duration":35.165624999999864,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing foundation: HTTP upload locations reject invalid credentials and another installation","status":"passed","title":"foundation: HTTP upload locations reject invalid credentials and another installation","duration":44.8326669999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing PUB-003-F: missing, truncated, oversized, and fingerprint-mismatched staged files cannot commit","status":"passed","title":"PUB-003-F: missing, truncated, oversized, and fingerprint-mismatched staged files cannot commit","duration":39.30458300000009,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451690414,"endTime":1788451691375.3047,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/complete-site.test.ts"},{"assertionResults":[{"ancestorTitles":["content access lifecycle"],"fullName":"content access lifecycle foundation: bootstrap and content-session expiration fail closed","status":"passed","title":"foundation: bootstrap and content-session expiration fail closed","duration":143.03087499999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["content access lifecycle"],"fullName":"content access lifecycle bootstrap replay, tampering, and host substitution do not grant a session","status":"passed","title":"bootstrap replay, tampering, and host substitution do not grant a session","duration":120.26791700000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["content access lifecycle"],"fullName":"content access lifecycle preview lease foundation: tampered leases fail and valid leases expire at their exact boundary","status":"passed","title":"preview lease foundation: tampered leases fail and valid leases expire at their exact boundary","duration":58.709957999999915,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451694331,"endTime":1788451694652.71,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/content-access-lifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["registered agent registry"],"fullName":"registered agent registry DSP-001-B: one connection key upserts one self-named agent whose connected state follows its claim polling","status":"passed","title":"DSP-001-B: one connection key upserts one self-named agent whose connected state follows its claim polling","duration":3417.1040829999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["registered agent registry"],"fullName":"registered agent registry DSP-001-F: reaping a stale agent row leaves its dispatch attributed, and another installation's agent never appears","status":"passed","title":"DSP-001-F: reaping a stale agent row leaves its dispatch attributed, and another installation's agent never appears","duration":222.12270799999988,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687670,"endTime":1788451691309.1228,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-001-agent-registry.test.ts"},{"assertionResults":[{"ancestorTitles":["agent connection authority"],"fullName":"agent connection authority DSP-002-B: an agent-connect key and the local all-capability token register, claim, and report delivered and failed","status":"passed","title":"DSP-002-B: an agent-connect key and the local all-capability token register, claim, and report delivered and failed","duration":555.5588339999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent connection authority"],"fullName":"agent connection authority DSP-002-F: a read-only key and a direct human are refused every agent operation, and a report from a non-holder is a state conflict","status":"passed","title":"DSP-002-F: a read-only key and a direct human are refused every agent operation, and a report from a non-holder is a state conflict","duration":302.20120899999984,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696074,"endTime":1788451696932.2012,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-002-agent-connect-authority.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch send"],"fullName":"agent dispatch send DSP-003-B: one send becomes one ordered dispatch, and the same idempotency key replays the original","status":"passed","title":"DSP-003-B: one send becomes one ordered dispatch, and the same idempotency key replays the original","duration":921.8779160000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch send"],"fullName":"agent dispatch send DSP-003-F: an empty, oversized, cross-project, resolved, or already dispatched bundle is rejected without marking a thread","status":"passed","title":"DSP-003-F: an empty, oversized, cross-project, resolved, or already dispatched bundle is rejected without marking a thread","duration":237.94691699999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695870,"endTime":1788451697029.947,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-003-dispatch-send.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatched thread visibility"],"fullName":"dispatched thread visibility DSP-004-B: HTTP, MCP, and the web application hide a sent bundle by default and answer the same for every dispatched filter","status":"passed","title":"DSP-004-B: HTTP, MCP, and the web application hide a sent bundle by default and answer the same for every dispatched filter","duration":339.789333,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatched thread visibility"],"fullName":"dispatched thread visibility DSP-004-F: no default listing on any surface can show a dispatched thread, and the filter never reaches another project or installation","status":"passed","title":"DSP-004-F: no default listing on any surface can show a dispatched thread, and the filter never reaches another project or installation","duration":268.555792,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696811,"endTime":1788451697419.556,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-004-dispatched-thread-visibility.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatch claim order"],"fullName":"dispatch claim order DSP-005-B: three queued bundles are claimed oldest first, one at a time, and a waiting poll answers inside its bound","status":"passed","title":"DSP-005-B: three queued bundles are claimed oldest first, one at a time, and a waiting poll answers inside its bound","duration":2387.001458,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatch claim order"],"fullName":"dispatch claim order DSP-005-F: a second claim while one is held answers empty, and simultaneous claims never hand one bundle to two agents","status":"passed","title":"DSP-005-F: a second claim while one is held answers empty, and simultaneous claims never hand one bundle to two agents","duration":2088.60225,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687669,"endTime":1788451692144.6023,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-005-claim-order.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatch claim lease"],"fullName":"dispatch claim lease DSP-006-B: an expired lease returns the bundle to the queue and the restarted agent claims it again","status":"passed","title":"DSP-006-B: an expired lease returns the bundle to the queue and the restarted agent claims it again","duration":145.30775,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatch claim lease"],"fullName":"dispatch claim lease DSP-006-F: a claimer that returns after its lease expired cannot report, and a reclaim never leaves two claims active","status":"passed","title":"DSP-006-F: a claimer that returns after its lease expired cannot report, and a reclaim never leaves two claims active","duration":379.0167090000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693493,"endTime":1788451694018.0166,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-006-claim-lease.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch state machine"],"fullName":"agent dispatch state machine DSP-007-B: one dispatch walks queued, claimed, delivered, and addressed with its own stamp on every transition","status":"passed","title":"DSP-007-B: one dispatch walks queued, claimed, delivered, and addressed with its own stamp on every transition","duration":437.05720799999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch state machine"],"fullName":"agent dispatch state machine DSP-007-F: every transition outside the machine is a state conflict that moves no recorded time","status":"passed","title":"DSP-007-F: every transition outside the machine is a state conflict that moves no recorded time","duration":374.72962499999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696099,"endTime":1788451696910.7297,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-007-dispatch-state-machine.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch addressed inference"],"fullName":"agent dispatch addressed inference DSP-008-B: a delivered bundle becomes addressed once every thread resolves, stamped when the read first observes it","status":"passed","title":"DSP-008-B: a delivered bundle becomes addressed once every thread resolves, stamped when the read first observes it","duration":365.188125,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch addressed inference"],"fullName":"agent dispatch addressed inference DSP-008-F: an unresolved thread holds the dispatch delivered, and reopening one afterwards neither undoes nor restamps addressed","status":"passed","title":"DSP-008-F: an unresolved thread holds the dispatch delivered, and reopening one afterwards neither undoes nor restamps addressed","duration":245.22704199999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698827,"endTime":1788451699438.227,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-008-addressed-inference.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch thread release"],"fullName":"agent dispatch thread release DSP-009-B: failure, cancellation, and an agent that stops polling each release their threads back into the default listings","status":"passed","title":"DSP-009-B: failure, cancellation, and an agent that stops polling each release their threads back into the default listings","duration":334.32054200000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch thread release"],"fullName":"agent dispatch thread release DSP-009-F: an addressed dispatch releases nothing and a held thread joins no second dispatch until it is released","status":"passed","title":"DSP-009-F: an addressed dispatch releases nothing and a held thread joins no second dispatch until it is released","duration":427.95995800000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696012,"endTime":1788451696773.96,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-009-dispatch-release.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch authorization"],"fullName":"agent dispatch authorization DSP-010-B: a human member and a manage-any key send and cancel while read authority lists and reads dispatches","status":"passed","title":"DSP-010-B: a human member and a manage-any key send and cancel while read authority lists and reads dispatches","duration":399.61645799999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch authorization"],"fullName":"agent dispatch authorization a foreign agent-connect key cannot claim or report for an agent it did not register","status":"passed","title":"a foreign agent-connect key cannot claim or report for an agent it did not register","duration":64.795208,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch authorization"],"fullName":"agent dispatch authorization DSP-010-F: an agent-connect key can neither send nor cancel, and identifiers from another project or installation are not found without disclosure","status":"passed","title":"DSP-010-F: an agent-connect key can neither send nor cancel, and identifiers from another project or installation are not found without disclosure","duration":133.91620899999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687670,"endTime":1788451688267.9163,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-010-dispatch-authorization.test.ts"},{"assertionResults":[{"ancestorTitles":["bridge bundle rendering"],"fullName":"bridge bundle rendering DSP-011-B: a bundle renders as one message with sender line, note, numbered items, quoted selection, bodies, thread ids, and the reply-and-resolve instruction, delivered as follow-up","status":"passed","title":"DSP-011-B: a bundle renders as one message with sender line, note, numbered items, quoted selection, bodies, thread ids, and the reply-and-resolve instruction, delivered as follow-up","duration":4475.178667,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge bundle rendering"],"fullName":"bridge bundle rendering DSP-011-F: hostile bundle text never yields a leading slash and every delivery names follow-up, never steering","status":"passed","title":"DSP-011-F: hostile bundle text never yields a leading slash and every delivery names follow-up, never steering","duration":1113.37525,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687689,"endTime":1788451693277.3752,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-011-bridge-bundle-render.test.ts"},{"assertionResults":[{"ancestorTitles":["bridge fail-open discipline"],"fullName":"bridge fail-open discipline DSP-012-B: without configuration one notice and no polling; configured, it registers, claims, holds through compaction, reports delivery, and shuts down cleanly","status":"passed","title":"DSP-012-B: without configuration one notice and no polling; configured, it registers, claims, holds through compaction, reports delivery, and shuts down cleanly","duration":5511.015584,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge fail-open discipline"],"fullName":"bridge fail-open discipline DSP-013-B: delivery waits for host admission, an asynchronous refusal fails only that dispatch, and the loop accepts the next bundle","status":"passed","title":"DSP-013-B: delivery waits for host admission, an asynchronous refusal fails only that dispatch, and the loop accepts the next bundle","duration":5231.943875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge fail-open discipline"],"fullName":"bridge fail-open discipline DSP-012-F: an unreachable backend backs off within bounds and a stale host handle ends the loop, with no error reaching the host","status":"passed","title":"DSP-012-F: an unreachable backend backs off within bounds and a stale host handle ends the loop, with no error reaching the host","duration":3295.1726670000007,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687689,"endTime":1788451701728.1726,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/dsp-012-bridge-fail-open.test.ts"},{"assertionResults":[{"ancestorTitles":["Git history capability monitoring"],"fullName":"Git history capability monitoring Git monitor lifecycle: a provider outage recovers and shutdown stops later checks","status":"passed","title":"Git monitor lifecycle: a provider outage recovers and shutdown stops later checks","duration":12.857708000000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699665,"endTime":1788451699677.8577,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/git-history-capability-monitor.test.ts"},{"assertionResults":[{"ancestorTitles":["optional Git handoff configuration and discovery"],"fullName":"optional Git handoff configuration and discovery GIT-010 GIT-012 GIT-014 foundation: absent and off configurations expose bounded disabled defaults without reading provider companions","status":"passed","title":"GIT-010 GIT-012 GIT-014 foundation: absent and off configurations expose bounded disabled defaults without reading provider companions","duration":4.656833000000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["optional Git handoff configuration and discovery"],"fullName":"optional Git handoff configuration and discovery GIT-010 GIT-012 foundation: complete Node REST configuration is parsed, while hostile or malformed families degrade without exposing secrets","status":"passed","title":"GIT-010 GIT-012 foundation: complete Node REST configuration is parsed, while hostile or malformed families degrade without exposing secrets","duration":14.223666999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["optional Git handoff configuration and discovery"],"fullName":"optional Git handoff configuration and discovery GIT-010 GIT-013 foundation: a successful read-only check persists identity, reports live parity, and rejects a changed destination before another provider call","status":"passed","title":"GIT-010 GIT-013 foundation: a successful read-only check persists identity, reports live parity, and rejects a changed destination before another provider call","duration":146.42320900000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699526,"endTime":1788451699692.423,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/git-history-discovery.test.ts"},{"assertionResults":[{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-008-B GIT-012-B: an explicitly enabled project backfills while every other project stays off","status":"passed","title":"GIT-008-B GIT-012-B: an explicitly enabled project backfills while every other project stays off","duration":8123.283041999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting foundation: an unconfigured deployment cannot estimate or enable, and the default remains off","status":"passed","title":"foundation: an unconfigured deployment cannot estimate or enable, and the default remains off","duration":32.295374999999694,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-009-F: retrying an already-enabled setting succeeds during a provider outage","status":"passed","title":"GIT-009-F: retrying an already-enabled setting succeeds during a provider outage","duration":39.3817080000008,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-002-F GIT-003-F GIT-008-F: a failed mirror resumes after process restart without a duplicate commit","status":"passed","title":"GIT-002-F GIT-003-F GIT-008-F: a failed mirror resumes after process restart without a duplicate commit","duration":6348.938334,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-005-F GIT-009-B: clone access is authorized and bounded, disablement preserves the repository, and artifact deletion removes it","status":"passed","title":"GIT-005-F GIT-009-B: clone access is authorized and bounded, disablement preserves the repository, and artifact deletion removes it","duration":4041.952624999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-004-B GIT-004-F: copy limits produce deterministic files and pointers across retry","status":"passed","title":"GIT-004-B GIT-004-F: copy limits produce deterministic files and pointers across retry","duration":4059.238458,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-014-B GIT-014-F: a logical budget pauses only Git and resumes after an explicit budget increase","status":"passed","title":"GIT-014-B GIT-014-F: a logical budget pauses only Git and resumes after an explicit budget increase","duration":4108.850375000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-011-B GIT-011-F: purge plans without provider access and resumes after partial deletion","status":"passed","title":"GIT-011-B GIT-011-F: purge plans without provider access and resumes after partial deletion","duration":2039.627375,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451687684,"endTime":1788451716478.6274,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/git-history-project-setting.test.ts"},{"assertionResults":[{"ancestorTitles":["Cloudflare Artifacts REST availability"],"fullName":"Cloudflare Artifacts REST availability GIT-010 GIT-013 foundation: the read-only namespace check is path-scoped, authenticated, bounded, and safely classified","status":"passed","title":"GIT-010 GIT-013 foundation: the read-only namespace check is path-scoped, authenticated, bounded, and safely classified","duration":40.61824999999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699595,"endTime":1788451699635.6182,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/git-history-provider-health.test.ts"},{"assertionResults":[{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-024-F AUTH-027-B AUTH-027-F AUTH-028-F: bootstrap membership and managed keys fail closed","status":"passed","title":"AUTH-024-F AUTH-027-B AUTH-027-F AUTH-028-F: bootstrap membership and managed keys fail closed","duration":223.38887500000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-023-B AUTH-023-F AUTH-024-B: local-owner browsers receive a stable session only across the loopback same-origin boundary","status":"passed","title":"AUTH-023-B AUTH-023-F AUTH-024-B: local-owner browsers receive a stable session only across the loopback same-origin boundary","duration":83.56016700000009,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-025-B AUTH-025-F AUTH-026-F: private-team mode advertises its provider and has no local browser bootstrap route","status":"passed","title":"AUTH-025-B AUTH-025-F AUTH-026-F: private-team mode advertises its provider and has no local browser bootstrap route","duration":116.056375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access concurrent private-team startup cannot seed two machine authorities","status":"passed","title":"concurrent private-team startup cannot seed two machine authorities","duration":64.76750000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-010-B AUTH-010-F AUTH-011-B AUTH-011-F AUTH-012-B AUTH-012-F: browser credentials stay on the application host and mutations require same-origin proof","status":"passed","title":"AUTH-010-B AUTH-010-F AUTH-011-B AUTH-011-F AUTH-012-B AUTH-012-F: browser credentials stay on the application host and mutations require same-origin proof","duration":56.487708999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-022-B AUTH-022-F: session authentication caching stays a bounded-staleness optimization","status":"passed","title":"AUTH-022-B AUTH-022-F: session authentication caching stays a bounded-staleness optimization","duration":74.70204200000012,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-001-B AUTH-001-F AUTH-028-B: external login admits only the configured first administrator and consumes state once","status":"passed","title":"AUTH-001-B AUTH-001-F AUTH-028-B: external login admits only the configured first administrator and consumes state once","duration":93.64779199999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access external login reports a malformed verified identity as a typed conflict","status":"passed","title":"external login reports a malformed verified identity as a typed conflict","duration":31.811542000000145,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access a configured external bearer verifier shares the principal boundary without managed-key parser fallback","status":"passed","title":"a configured external bearer verifier shares the principal boundary without managed-key parser fallback","duration":27.885125000000016,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access API OAuth metadata and challenge name the exact API resource without enabling it by default","status":"passed","title":"API OAuth metadata and challenge name the exact API resource without enabling it by default","duration":30.73112500000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451692604,"endTime":1788451693407.7312,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/installation-identity.test.ts"},{"assertionResults":[{"ancestorTitles":["linked artifacts are off by default and reference in place"],"fullName":"linked artifacts are off by default and reference in place LNK-001-B: an enabled local deployment advertises the capability, links one file in place over HTTP and MCP, and never absorbs an unlinked file","status":"passed","title":"LNK-001-B: an enabled local deployment advertises the capability, links one file in place over HTTP and MCP, and never absorbs an unlinked file","duration":303.20087499999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked artifacts are off by default and reference in place"],"fullName":"linked artifacts are off by default and reference in place LNK-001-F: with the feature off the capability is absent, every linked route and tool answers the stable shape, and no file becomes an artifact","status":"passed","title":"LNK-001-F: with the feature off the capability is absent, every linked route and tool answers the stable shape, and no file becomes an artifact","duration":94.76354199999992,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696833,"endTime":1788451697230.7634,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-001-off-by-default.test.ts"},{"assertionResults":[{"ancestorTitles":["linking a file publishes an ordinary first version"],"fullName":"linking a file publishes an ordinary first version LNK-002-B: one linked regular file becomes an artifact whose first version carries its verified bytes, manifest, and action","status":"passed","title":"LNK-002-B: one linked regular file becomes an artifact whose first version carries its verified bytes, manifest, and action","duration":269.95141699999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linking a file publishes an ordinary first version"],"fullName":"linking a file publishes an ordinary first version LNK-002-F: a path that is not a readable regular file creates nothing, and replaying one link key returns the original artifact","status":"passed","title":"LNK-002-F: a path that is not a readable regular file creates nothing, and replaying one link key returns the original artifact","duration":320.38200000000006,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693781,"endTime":1788451694371.382,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-002-link-initial-capture.test.ts"},{"assertionResults":[{"ancestorTitles":["linked source freshness on ordinary metadata reads"],"fullName":"linked source freshness on ordinary metadata reads LNK-003-B: external modification, replacement, and removal move freshness while the version list and delivered bytes hold still","status":"passed","title":"LNK-003-B: external modification, replacement, and removal move freshness while the version list and delivered bytes hold still","duration":268.538875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source freshness on ordinary metadata reads"],"fullName":"linked source freshness on ordinary metadata reads LNK-003-F: a refresh cannot write a version or an action, and a failed stat degrades only the freshness field","status":"passed","title":"LNK-003-F: a refresh cannot write a version or an action, and a failed stat degrades only the freshness field","duration":423.95504099999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697350,"endTime":1788451698042.955,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-003-binding-freshness.test.ts"},{"assertionResults":[{"ancestorTitles":["capturing a linked source as a new immutable version"],"fullName":"capturing a linked source as a new immutable version LNK-004-B: capture publishes the current source bytes as a new version and an in-sync capture replays the current one","status":"passed","title":"LNK-004-B: capture publishes the current source bytes as a new version and an in-sync capture replays the current one","duration":172.37124999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["capturing a linked source as a new immutable version"],"fullName":"capturing a linked source as a new immutable version LNK-004-F: a stale expected current version returns the publish conflict with the actual current version and no version is written","status":"passed","title":"LNK-004-F: a stale expected current version returns the publish conflict with the actual current version and no version is written","duration":204.50437499999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451691743,"endTime":1788451692120.5044,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-004-source-capture.test.ts"},{"assertionResults":[{"ancestorTitles":["relinking one binding at a moved source file"],"fullName":"relinking one binding at a moved source file LNK-005-B: a moved file relinks under its expected hash and keeps the artifact, versions, and comment threads","status":"passed","title":"LNK-005-B: a moved file relinks under its expected hash and keeps the artifact, versions, and comment threads","duration":173.04191599999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["relinking one binding at a moved source file"],"fullName":"relinking one binding at a moved source file LNK-005-F: a mismatched hash, a path outside every root, and a path inside the data directory all change nothing","status":"passed","title":"LNK-005-F: a mismatched hash, a path outside every root, and a path inside the data directory all change nothing","duration":99.86870799999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695554,"endTime":1788451695826.8687,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-005-relink-moved-source.test.ts"},{"assertionResults":[{"ancestorTitles":["linked source paths are canonicalized, root-checked, and self-protected"],"fullName":"linked source paths are canonicalized, root-checked, and self-protected LNK-006-B: an admitted member links inside a root over loopback and reads the path only through authenticated metadata","status":"passed","title":"LNK-006-B: an admitted member links inside a root over loopback and reads the path only through authenticated metadata","duration":219.06350000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source paths are canonicalized, root-checked, and self-protected"],"fullName":"linked source paths are canonicalized, root-checked, and self-protected LNK-006-F: every escape, protected target, unauthorized caller, and non-loopback origin is refused without disclosing a path","status":"passed","title":"LNK-006-F: every escape, protected target, unauthorized caller, and non-loopback origin is refused without disclosing a path","duration":137.289083,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451692625,"endTime":1788451692981.289,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-006-path-safety.test.ts"},{"assertionResults":[{"ancestorTitles":["the live view reaches only authenticated local members"],"fullName":"the live view reaches only authenticated local members LNK-007-B: a local member streams the live bytes no-store while captured versions stay immutably cached","status":"passed","title":"LNK-007-B: a local member streams the live bytes no-store while captured versions stay immutably cached","duration":135.99862499999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["the live view reaches only authenticated local members"],"fullName":"the live view reaches only authenticated local members LNK-007-F: anonymous, cross-origin, public-link, and non-member readers never receive a live byte, and a missing source degrades to the capture","status":"passed","title":"LNK-007-F: anonymous, cross-origin, public-link, and non-member readers never receive a live byte, and a missing source degrades to the capture","duration":88.89300000000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451694858,"endTime":1788451695082.893,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-007-live-view-audience.test.ts"},{"assertionResults":[{"ancestorTitles":["comment threads on a linked artifact anchor to captured versions"],"fullName":"comment threads on a linked artifact anchor to captured versions LNK-008-B: an in-sync thread anchors to the current version and a drifted one implicitly captures for the commenting principal","status":"passed","title":"LNK-008-B: an in-sync thread anchors to the current version and a drifted one implicitly captures for the commenting principal","duration":306.107292,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment threads on a linked artifact anchor to captured versions"],"fullName":"comment threads on a linked artifact anchor to captured versions LNK-008-F: an aborted implicit capture fails the whole comment request and leaves no thread and no version","status":"passed","title":"LNK-008-F: an aborted implicit capture fails the whole comment request and leaves no thread and no version","duration":511.7683750000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695983,"endTime":1788451696800.7683,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/lnk-008-comment-implicit-capture.test.ts"},{"assertionResults":[{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP MCP-001-B MCP-001-F MCP-002-B MCP-002-F MCP-003-B MCP-003-F MCP-004-B MCP-004-F MCP-005-B MCP-005-F: modern discovery is authenticated, stateless, and exact","status":"passed","title":"MCP-001-B MCP-001-F MCP-002-B MCP-002-F MCP-003-B MCP-003-F MCP-004-B MCP-004-F MCP-005-B MCP-005-F: modern discovery is authenticated, stateless, and exact","duration":670.802041,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP MCP-006-B MCP-006-F MCP-007-B MCP-007-F MCP-008-B MCP-008-F MCP-015-B MCP-015-F PUB-013-B PUB-013-F: every advertised artifact operation runs over real files and shared policy","status":"passed","title":"MCP-006-B MCP-006-F MCP-007-B MCP-007-F MCP-008-B MCP-008-F MCP-015-B MCP-015-F PUB-013-B PUB-013-F: every advertised artifact operation runs over real files and shared policy","duration":301.3531250000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP MCP manages projects and refuses ambiguous artifact scope","status":"passed","title":"MCP manages projects and refuses ambiguous artifact scope","duration":78.16583400000013,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP external credentials use the same installation and capability policy as HTTP","status":"passed","title":"external credentials use the same installation and capability policy as HTTP","duration":57.59024999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP the linked-file tools answer the stable coded error while the capability is off","status":"passed","title":"the linked-file tools answer the stable coded error while the capability is off","duration":43.247249999999894,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP the linked-file tools link, capture, and relink one file on the server machine","status":"passed","title":"the linked-file tools link, capture, and relink one file on the server machine","duration":175.46974999999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451688695,"endTime":1788451690022.4697,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/mcp-modern-http.test.ts"},{"assertionResults":[{"ancestorTitles":["queued-work nudges on MCP tool results"],"fullName":"queued-work nudges on MCP tool results NDG-001-B: a principal with queued mailbox dispatches sees exactly one queued-work nudge on an unrelated tool result, the nudge names the real count and steps down to the unfinished claim and the settling echo as the queue drains, and it disappears once nothing is pending","status":"passed","title":"NDG-001-B: a principal with queued mailbox dispatches sees exactly one queued-work nudge on an unrelated tool result, the nudge names the real count and steps down to the unfinished claim and the settling echo as the queue drains, and it disappears once nothing is pending","duration":441.213542,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["queued-work nudges on MCP tool results"],"fullName":"queued-work nudges on MCP tool results NDG-001-F: with work queued, an error result and a dispatch_inbox result each carry no nudge","status":"passed","title":"NDG-001-F: with work queued, an error result and a dispatch_inbox result each carry no nudge","duration":142.930292,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451691790,"endTime":1788451692373.9302,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/ndg-001-queued-work-nudge.test.ts"},{"assertionResults":[{"ancestorTitles":["nudge isolation on MCP tool results"],"fullName":"nudge isolation on MCP tool results NDG-002-B: structuredContent and the existing content items are byte-identical with and without a pending nudge, and the nudge is one bounded trailing item that quotes no untrusted text","status":"passed","title":"NDG-002-B: structuredContent and the existing content items are byte-identical with and without a pending nudge, and the nudge is one bounded trailing item that quotes no untrusted text","duration":390.490833,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["nudge isolation on MCP tool results"],"fullName":"nudge isolation on MCP tool results NDG-002-F: a second principal calling the same tools sees none of the first principal's counts, with or without a mailbox agent of its own","status":"passed","title":"NDG-002-F: a second principal calling the same tools sees none of the first principal's counts, with or without a mailbox agent of its own","duration":406.0120830000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693707,"endTime":1788451694504.012,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/ndg-002-nudge-isolation.test.ts"},{"assertionResults":[{"ancestorTitles":["private content sessions"],"fullName":"private content sessions AUTH-014-B AUTH-015-B: one bootstrap opens one private immutable site through a host-only session","status":"passed","title":"AUTH-014-B AUTH-015-B: one bootstrap opens one private immutable site through a host-only session","duration":427.59495799999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["private content sessions"],"fullName":"private content sessions AUTH-015-F: content sessions cannot cross version hosts or authorize writes","status":"passed","title":"AUTH-015-F: content sessions cannot cross version hosts or authorize writes","duration":329.924083,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["private content sessions"],"fullName":"private content sessions CMT-018-B CMT-018-F: an authenticated exact-version preview lease is read-only, restart-safe, and version-bound","status":"passed","title":"CMT-018-B CMT-018-F: an authenticated exact-version preview lease is read-only, restart-safe, and version-bound","duration":217.2048339999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697712,"endTime":1788451698687.2048,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/private-content-session.test.ts"},{"assertionResults":[{"ancestorTitles":["project-scoped artifacts"],"fullName":"project-scoped artifacts default use stays simple while project boundaries stay isolated","status":"passed","title":"default use stays simple while project boundaries stay isolated","duration":360.50920799999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["project-scoped artifacts"],"fullName":"project-scoped artifacts archive blocks writes but preserves exact history","status":"passed","title":"archive blocks writes but preserves exact history","duration":238.986666,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698770,"endTime":1788451699368.9866,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/project-scoped-artifacts.test.ts"},{"assertionResults":[{"ancestorTitles":["derived agent activity"],"fullName":"derived agent activity PRS-001-B: a fresh agent reads idle, a claimed bundle reads working with its dispatch id, and full resolution returns it to idle with the dispatch addressed","status":"passed","title":"PRS-001-B: a fresh agent reads idle, a claimed bundle reads working with its dispatch id, and full resolution returns it to idle with the dispatch addressed","duration":245.78216699999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["derived agent activity"],"fullName":"derived agent activity PRS-001-F: a stale heartbeat reads disconnected while the agent still holds a claim, and repeated reads write no activity","status":"passed","title":"PRS-001-F: a stale heartbeat reads disconnected while the agent still holds a claim, and repeated reads write no activity","duration":140.35779200000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695084,"endTime":1788451695470.358,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/prs-001-derived-activity.test.ts"},{"assertionResults":[{"ancestorTitles":["agent activity beacon"],"fullName":"agent activity beacon PRS-002-B: a fresh beacon reports working with its finer state, decays to the derived state after the 60 s TTL, and an idle beacon clears it at once","status":"passed","title":"PRS-002-B: a fresh beacon reports working with its finer state, decays to the derived state after the 60 s TTL, and an idle beacon clears it at once","duration":204.92729200000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent activity beacon"],"fullName":"agent activity beacon PRS-002-F: a beacon from an agent whose heartbeat is stale is accepted with 204 but never surfaces in the listing","status":"passed","title":"PRS-002-F: a beacon from an agent whose heartbeat is stale is accepted with 204 but never surfaces in the listing","duration":41.92087499999991,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699218,"endTime":1788451699464.921,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/prs-002-activity-beacon.test.ts"},{"assertionResults":[{"ancestorTitles":["activity beacon authority"],"fullName":"activity beacon authority PRS-003-B: an agent principal beacons its own agent and the finer state surfaces","status":"passed","title":"PRS-003-B: an agent principal beacons its own agent and the finer state surfaces","duration":212.52929200000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["activity beacon authority"],"fullName":"activity beacon authority PRS-003-F: another principal's beacon answers the same 404 as a missing agent and records nothing, and a direct human is refused outright","status":"passed","title":"PRS-003-F: another principal's beacon answers the same 404 as a missing agent and records nothing, and a direct human is refused outright","duration":51.741833999999926,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699223,"endTime":1788451699486.742,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/prs-003-beacon-authority.test.ts"},{"assertionResults":[{"ancestorTitles":["bulk comment clearing"],"fullName":"bulk comment clearing PRS-005-B: clearing resolved deletes exactly the resolved threads with one ledger action each, and a manage-any key clears all","status":"passed","title":"PRS-005-B: clearing resolved deletes exactly the resolved threads with one ledger action each, and a manage-any key clears all","duration":385.18066699999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bulk comment clearing"],"fullName":"bulk comment clearing PRS-005-F: an actively dispatched thread is skipped and counted, a non-member key is refused, and MCP comment_clear answers the same shape","status":"passed","title":"PRS-005-F: an actively dispatched thread is skipped and counted, a non-member key is refused, and MCP comment_clear answers the same shape","duration":322.628291,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697544,"endTime":1788451698251.6282,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/prs-005-bulk-clear.test.ts"},{"assertionResults":[{"ancestorTitles":["registered agent kind widening"],"fullName":"registered agent kind widening PRS-007-B: any valid kind slug registers, round-trips through the listing, and the response carries protocol version 1","status":"passed","title":"PRS-007-B: any valid kind slug registers, round-trips through the listing, and the response carries protocol version 1","duration":196.91983300000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["registered agent kind widening"],"fullName":"registered agent kind widening PRS-007-F: a kind that is not a slug is refused with 422 and registers nothing","status":"passed","title":"PRS-007-F: a kind that is not a slug is refused with 422 and registers nothing","duration":90.29787499999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["registered agent kind widening"],"fullName":"registered agent kind widening PRS-007: declared capabilities normalize to known keys, unknown keys are ignored, and an absent declaration defaults to the native tier","status":"passed","title":"PRS-007: declared capabilities normalize to known keys, unknown keys are ignored, and an absent declaration defaults to the native tier","duration":59.13554199999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451695220,"endTime":1788451695566.1355,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/prs-007-agent-kind-slug.test.ts"},{"assertionResults":[{"ancestorTitles":["authenticated media preview delivery"],"fullName":"authenticated media preview delivery PRV-003-B: exact image and ranged video bytes use typed, immutable, media-only responses","status":"passed","title":"PRV-003-B: exact image and ranged video bytes use typed, immutable, media-only responses","duration":227.52441699999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated media preview delivery"],"fullName":"authenticated media preview delivery PRV-003-F: hostile contexts, unsupported types, and absent paths fail closed","status":"passed","title":"PRV-003-F: hostile contexts, unsupported types, and absent paths fail closed","duration":176.08495800000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451692831,"endTime":1788451693234.085,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/prv-003-media-preview-route.test.ts"},{"assertionResults":[{"ancestorTitles":["public-link administration"],"fullName":"public-link administration AUTH-018-B: administrators page across projects and make one public artifact private","status":"passed","title":"AUTH-018-B: administrators page across projects and make one public artifact private","duration":630.272833,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["public-link administration"],"fullName":"public-link administration AUTH-018-F: bulk shutdown rejects invalid requests and preserves partial-failure retry semantics","status":"passed","title":"AUTH-018-F: bulk shutdown rejects invalid requests and preserves partial-failure retry semantics","duration":453.9909170000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697407,"endTime":1788451698490.991,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/public-link-administration.test.ts"},{"assertionResults":[{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime ART-001-B: versions keep identity while publication links keep their distinct roles","status":"passed","title":"ART-001-B: versions keep identity while publication links keep their distinct roles","duration":406.583625,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime PUB-006-B PUB-006-F: retries return the original result and conflicting reuse fails","status":"passed","title":"PUB-006-B PUB-006-F: retries return the original result and conflicting reuse fails","duration":266.3299169999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime PUB-008-B PUB-008-F: concurrent stale publishes cannot overwrite the winner","status":"passed","title":"PUB-008-B PUB-008-F: concurrent stale publishes cannot overwrite the winner","duration":199.22474999999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime PUB-004-B PUB-007-B: an intentional identical publish creates a version but reuses immutable bytes","status":"passed","title":"PUB-004-B PUB-007-B: an intentional identical publish creates a version but reuses immutable bytes","duration":219.42737499999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451697431,"endTime":1788451698522.4275,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/publishing-runtime.test.ts"},{"assertionResults":[{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries MAN-003-B MAN-003-F: portable paths round-trip through URLs and unsafe paths are rejected","status":"passed","title":"MAN-003-B MAN-003-F: portable paths round-trip through URLs and unsafe paths are rejected","duration":149.878917,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries API publication requires a trusted principal and creates nothing on rejection","status":"passed","title":"API publication requires a trusted principal and creates nothing on rejection","duration":61.84362500000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries AUTH-002-B: account-required is the default and an admitted principal can open it","status":"passed","title":"AUTH-002-B: account-required is the default and an admitted principal can open it","duration":53.73625000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries PUB-002-F SCP-007-F: public APIs reject raw contents, paths, and source URLs","status":"passed","title":"PUB-002-F SCP-007-F: public APIs reject raw contents, paths, and source URLs","duration":24.881916000000047,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries foundation: malformed and oversized file-upload plans fail without destabilizing the server","status":"passed","title":"foundation: malformed and oversized file-upload plans fail without destabilizing the server","duration":538.820875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries foundation: the local server binds only to IPv4 loopback","status":"passed","title":"foundation: the local server binds only to IPv4 loopback","duration":15.610292000000072,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries AUTH-002-F: account-required bytes do not open through stable or exact links","status":"passed","title":"AUTH-002-F: account-required bytes do not open through stable or exact links","duration":47.57033299999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries foundation: public content is retained at the edge but authorization is revalidated","status":"passed","title":"foundation: public content is retained at the edge but authorization is revalidated","duration":77.16612500000019,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451689411,"endTime":1788451690381.166,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/publishing-security.test.ts"},{"assertionResults":[{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle PUB-001-F: another principal or installation cannot use a staged upload","status":"passed","title":"PUB-001-F: another principal or installation cannot use a staged upload","duration":280.23383300000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle expiry closes only uncommitted staging while committed retries remain stable","status":"passed","title":"expiry closes only uncommitted staging while committed retries remain stable","duration":345.6279169999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle PUB-009-B OPS-006-F: cleanup is bounded, waits for settle, tolerates concurrent passes, and preserves committed work","status":"passed","title":"PUB-009-B OPS-006-F: cleanup is bounded, waits for settle, tolerates concurrent passes, and preserves committed work","duration":123.30612500000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle foundation: an interrupted local write cannot reappear after cleanup","status":"passed","title":"foundation: an interrupted local write cannot reappear after cleanup","duration":45.909834000000046,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696148,"endTime":1788451696943.91,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/staged-upload-lifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["local storage migration"],"fullName":"local storage migration foundation: an early local database upgrades without losing publication","status":"passed","title":"foundation: an early local database upgrades without losing publication","duration":329.12012500000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local storage migration"],"fullName":"local storage migration a populated pre-project database migrates once without changing identity or bytes","status":"passed","title":"a populated pre-project database migrates once without changing identity or bytes","duration":272.6260410000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local storage migration"],"fullName":"local storage migration foundation: the previous management schema upgrades to durable tag and deletion idempotency","status":"passed","title":"foundation: the previous management schema upgrades to durable tag and deletion idempotency","duration":267.90658400000007,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451696060,"endTime":1788451696929.9065,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/storage-migration.test.ts"},{"assertionResults":[{"ancestorTitles":["hosted WorkOS MCP authorization"],"fullName":"hosted WorkOS MCP authorization hosted startup accepts only exact refreshable S256 authorization metadata","status":"passed","title":"hosted startup accepts only exact refreshable S256 authorization metadata","duration":269.500125,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["hosted WorkOS MCP authorization"],"fullName":"hosted WorkOS MCP authorization foundation: discovery, JWT verification, first-use identity binding, and API-key fallback share one live HTTP boundary","status":"passed","title":"foundation: discovery, JWT verification, first-use identity binding, and API-key fallback share one live HTTP boundary","duration":437.1162499999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["hosted WorkOS MCP authorization"],"fullName":"hosted WorkOS MCP authorization foundation: wrong token contracts fail closed and provider outages stay distinct","status":"passed","title":"foundation: wrong token contracts fail closed and provider outages stay distinct","duration":182.55091700000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451693804,"endTime":1788451694693.551,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/conformance/workos-mcp-authorization.test.ts"},{"assertionResults":[{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization a relative path is rejected before any filesystem access","status":"passed","title":"a relative path is rejected before any filesystem access","duration":23.80983299999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization a missing target, a directory, and a dangling symlink are rejected","status":"passed","title":"a missing target, a directory, and a dangling symlink are rejected","duration":12.80400000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization a symlink resolves to its target so containment is checked on the real file","status":"passed","title":"a symlink resolves to its target so containment is checked on the real file","duration":8.65466699999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization error messages never include the presented path","status":"passed","title":"error messages never include the presented path","duration":12.441874999999982,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","link roots"],"fullName":"linked source engine link roots dot-dot traversal cannot escape a root because paths canonicalize first","status":"passed","title":"dot-dot traversal cannot escape a root because paths canonicalize first","duration":6.741124999999954,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","link roots"],"fullName":"linked source engine link roots a file inside any configured root passes","status":"passed","title":"a file inside any configured root passes","duration":4.381582999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","link roots"],"fullName":"linked source engine link roots a misconfigured root fails loudly instead of changing the boundary","status":"passed","title":"a misconfigured root fails loudly instead of changing the boundary","duration":5.588666999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","self-protection"],"fullName":"linked source engine self-protection the data directory, its children, and the database companions are always rejected","status":"passed","title":"the data directory, its children, and the database companions are always rejected","duration":15.752499999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","self-protection"],"fullName":"linked source engine self-protection a database that lives outside the data directory is still protected","status":"passed","title":"a database that lives outside the data directory is still protected","duration":17.105458,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","self-protection"],"fullName":"linked source engine self-protection an ordinary file outside the protected locations passes","status":"passed","title":"an ordinary file outside the protected locations passes","duration":13.702375000000018,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","freshness"],"fullName":"linked source engine freshness an untouched file is in-sync and a rewritten file is modified","status":"passed","title":"an untouched file is in-sync and a rewritten file is modified","duration":24.986999999999966,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","freshness"],"fullName":"linked source engine freshness a deleted file is missing and a permission-denied file is unreadable","status":"passed","title":"a deleted file is missing and a permission-denied file is unreadable","duration":44.67183399999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","freshness"],"fullName":"linked source engine freshness a regular file replaced by a symlink is unreadable, never followed","status":"passed","title":"a regular file replaced by a symlink is unreadable, never followed","duration":20.169791999999973,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open stat and bytes come from the same descriptor even when the path is swapped","status":"passed","title":"stat and bytes come from the same descriptor even when the path is swapped","duration":18.19479199999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open a symlink at the final path is refused by O_NOFOLLOW","status":"passed","title":"a symlink at the final path is refused by O_NOFOLLOW","duration":4.304124999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open an expected-fingerprint mismatch aborts with the retryable drift error","status":"passed","title":"an expected-fingerprint mismatch aborts with the retryable drift error","duration":8.217291999999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open a missing file surfaces as missing","status":"passed","title":"a missing file surfaces as missing","duration":3.6550000000000296,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","capture"],"fullName":"linked source engine capture capture hashes and spools exactly the bytes of one descriptor pass","status":"passed","title":"capture hashes and spools exactly the bytes of one descriptor pass","duration":41.435167000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","capture"],"fullName":"linked source engine capture a modification during the read aborts the capture and removes the spool","status":"passed","title":"a modification during the read aborts the capture and removes the spool","duration":19.796957999999904,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","capture"],"fullName":"linked source engine capture a capture of a path swapped to a symlink is refused","status":"passed","title":"a capture of a path swapped to a symlink is refused","duration":19.324833000000012,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698725,"endTime":1788451699051.325,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/local/linked-source-engine.test.ts"},{"assertionResults":[{"ancestorTitles":["bounded local runtime smoke"],"fullName":"bounded local runtime smoke publishes, reads concurrently, restarts, and stays within gross safety limits","status":"passed","title":"publishes, reads concurrently, restarts, and stays within gross safety limits","duration":1159.096375,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bounded local runtime smoke"],"fullName":"bounded local runtime smoke rejects a workload whose aggregate bytes would become a stress test","status":"passed","title":"rejects a workload whose aggregate bytes would become a stress test","duration":1.0294999999998709,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451692032,"endTime":1788451693193.0295,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/smoke/local-performance-smoke.test.ts"},{"assertionResults":[{"ancestorTitles":["native cloud object integrity helpers"],"fullName":"native cloud object integrity helpers an opened provider object must include the promised byte body","status":"passed","title":"an opened provider object must include the promised byte body","duration":1.0306659999999965,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["native cloud object integrity helpers"],"fullName":"native cloud object integrity helpers a completed provider upload must report the exact accepted size","status":"passed","title":"a completed provider upload must report the exact accepted size","duration":0.11983399999999733,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699720,"endTime":1788451699721.1199,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/storage/cloud-object-storage.test.ts"},{"assertionResults":[{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a link commit persists the first version and its source binding","status":"passed","title":"a link commit persists the first version and its source binding","duration":57.014917,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a link commit replays under its original idempotency key","status":"passed","title":"a link commit replays under its original idempotency key","duration":15.827916000000016,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings recording freshness changes only the observed state","status":"passed","title":"recording freshness changes only the observed state","duration":16.55291699999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a captured version lands as an ordinary version row","status":"passed","title":"a captured version lands as an ordinary version row","duration":24.220500000000015,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a relink keeps the artifact identity and every saved version","status":"passed","title":"a relink keeps the artifact identity and every saved version","duration":19.124124999999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a replayed relink appends no second action record","status":"passed","title":"a replayed relink appends no second action record","duration":37.46424999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings binding operations reject an artifact without a binding","status":"passed","title":"binding operations reject an artifact without a binding","duration":18.569334000000026,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings an ordinary publication leaves every binding column null","status":"passed","title":"an ordinary publication leaves every binding column null","duration":15.28004199999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings the stored schema rejects a partial or unknown binding state","status":"passed","title":"the stored schema rejects a partial or unknown binding state","duration":18.47637500000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451690617,"endTime":1788451690839.4763,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/storage/sqlite-source-binding.test.ts"},{"assertionResults":[{"ancestorTitles":["anti-slop Oxlint plugin"],"fullName":"anti-slop Oxlint plugin every configured rule rejects its representative unsafe pattern","status":"passed","title":"every configured rule rejects its representative unsafe pattern","duration":193.202958,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["anti-slop Oxlint plugin"],"fullName":"anti-slop Oxlint plugin the plugin accepts precise types, direct calls, parsing, and justified assertions","status":"passed","title":"the plugin accepts precise types, direct calls, parsing, and justified assertions","duration":143.339334,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451698115,"endTime":1788451698452.3394,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/tooling/anti-slop-plugin.test.ts"},{"assertionResults":[{"ancestorTitles":["object-storage provider boundary"],"fullName":"object-storage provider boundary DEP-022-F: provider SDK imports stay inside their adapters","status":"passed","title":"DEP-022-F: provider SDK imports stay inside their adapters","duration":38.48670799999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1788451699330,"endTime":1788451699368.4868,"status":"passed","message":"","name":"/Users/ramos/oss/artifact-server/tests/tooling/object-storage-boundary.test.ts"}]} \ No newline at end of file +{"numTotalTestSuites":216,"numPassedTestSuites":216,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":374,"numPassedTests":374,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1791572702659,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication CLI-003-B CLI-003-F: resumes a lost commit response across CLI process restarts","status":"passed","title":"CLI-003-B CLI-003-F: resumes a lost commit response across CLI process restarts","duration":8494.905146,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication CLI-002-B PUB-012-B: stores a verified profile outside the project and publishes through it after restart","status":"passed","title":"CLI-002-B PUB-012-B: stores a verified profile outside the project and publishes through it after restart","duration":24605.570821,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication CLI-002-F PUB-012-F: rejects an invalid credential without saving it and never sends one profile to another origin","status":"passed","title":"CLI-002-F PUB-012-F: rejects an invalid credential without saving it and never sends one profile to another origin","duration":16254.759915000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated CLI profiles and remote publication"],"fullName":"authenticated CLI profiles and remote publication completes browser PKCE, refresh, status, and revocation against real HTTP boundaries","status":"passed","title":"completes browser PKCE, refresh, status, and revocation against real HTTP boundaries","duration":8837.249921999995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572707322,"endTime":1791572765514.25,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/cli/auth-profile-remote-publish.test.ts"},{"assertionResults":[{"ancestorTitles":["Artifact Server lifecycle CLI"],"fullName":"Artifact Server lifecycle CLI foundation: initialization, configuration, and support diagnostics are stable and secret-free","status":"passed","title":"foundation: initialization, configuration, and support diagnostics are stable and secret-free","duration":14383.077550999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Artifact Server lifecycle CLI"],"fullName":"Artifact Server lifecycle CLI foundation: compact serving withdraws readiness and integrity scans real committed bytes","status":"passed","title":"foundation: compact serving withdraws readiness and integrity scans real committed bytes","duration":12496.968351999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572799745,"endTime":1791572826624.9683,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/cli/lifecycle-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link links a file named by a relative path and prints the binding","status":"passed","title":"links a file named by a relative path and prints the binding","duration":2532.5361890000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link explains the unavailable capability in one line and exits non-zero","status":"passed","title":"explains the unavailable capability in one line and exits non-zero","duration":2106.955908,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link refuses a path outside the configured link roots","status":"passed","title":"refuses a path outside the configured link roots","duration":1998.5474299999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link CLI-SEC-001-F: never sends a local-owner token to an arbitrary explicit origin","status":"passed","title":"CLI-SEC-001-F: never sends a local-owner token to an arbitrary explicit origin","duration":2233.6351319999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link CLI-SEC-002-F: refuses authenticated redirects","status":"passed","title":"CLI-SEC-002-F: refuses authenticated redirects","duration":2044.0769799999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifactserver link"],"fullName":"artifactserver link foundation: missing local credentials name real recovery commands","status":"passed","title":"foundation: missing local credentials name real recovery commands","duration":1985.4141100000015,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572991425,"endTime":1791573004326.414,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/cli/link-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI opens the managed application at a credential-free URL","status":"passed","title":"opens the managed application at a credential-free URL","duration":6651.280686,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI starts with private reusable credentials without printing them","status":"passed","title":"starts with private reusable credentials without printing them","duration":4735.250446999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI rejects an invalid port before creating persistent state","status":"passed","title":"rejects an invalid port before creating persistent state","duration":1926.5695719999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local Artifact Server CLI"],"fullName":"local Artifact Server CLI PUB-002-B SCP-007-B: publishes a file, a large finished directory, and a second immutable version through real processes","status":"passed","title":"PUB-002-B SCP-007-B: publishes a file, a large finished directory, and a second immutable version through real processes","duration":22080.649998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572858999,"endTime":1791572894393.65,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/cli/local-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding stdio reuses one service and preserves modern discovery","status":"passed","title":"stdio reuses one service and preserves modern discovery","duration":15321.914632,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding connect and disconnect preserve unrelated client configuration","status":"passed","title":"connect and disconnect preserve unrelated client configuration","duration":107.45003499999984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding foundation: native inspection failures stop before client mutation","status":"passed","title":"foundation: native inspection failures stop before client mutation","duration":5.755032999999457,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding MCP-020-B: resumes a journaled native-client change after process interruption","status":"passed","title":"MCP-020-B: resumes a journaled native-client change after process interruption","duration":703.5211869999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding MCP-020-F: rolls back failed mutation and preserves an independent interrupted change","status":"passed","title":"MCP-020-F: rolls back failed mutation and preserves an independent interrupted change","duration":741.7664549999972,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding refuses foreign and malformed JSON client configuration","status":"passed","title":"refuses foreign and malformed JSON client configuration","duration":2.6956339999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local MCP onboarding"],"fullName":"local MCP onboarding ambiguous connect and doctor failures do not create or repair state","status":"passed","title":"ambiguous connect and doctor failures do not create or repair state","duration":11610.753017000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572829030,"endTime":1791572857524.753,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/cli/mcp-onboarding.test.ts"},{"assertionResults":[{"ancestorTitles":["artifactserver start with linked files enabled"],"fullName":"artifactserver start with linked files enabled the environment flags reach the direct-start server and linking works","status":"passed","title":"the environment flags reach the direct-start server and linking works","duration":2197.4724659999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573044189,"endTime":1791573046386.4724,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/cli/start-linked-cli.test.ts"},{"assertionResults":[{"ancestorTitles":["bridge activity beacon"],"fullName":"bridge activity beacon beacons fire once each at bundle acceptance, first reply, and settled work","status":"passed","title":"beacons fire once each at bundle acceptance, first reply, and settled work","duration":4388.810783999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge activity beacon"],"fullName":"bridge activity beacon a failing beacon transport never disturbs delivery, replies, or the host","status":"passed","title":"a failing beacon transport never disturbs delivery, replies, or the host","duration":4094.3351089999996,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572978298,"endTime":1791572986781.3352,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/bridge-core-beacon.test.ts"},{"assertionResults":[{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization strips every bidirectional control while keeping the visible text","status":"passed","title":"strips every bidirectional control while keeping the visible text","duration":2.767989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization strips every zero-width and invisible character","status":"passed","title":"strips every zero-width and invisible character","duration":0.4587280000000078,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization defuses a trojan-source style reordering attack in one pass","status":"passed","title":"defuses a trojan-source style reordering attack in one pass","duration":0.24246199999998908,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization leaves ordinary multilingual text, whitespace, and punctuation alone","status":"passed","title":"leaves ordinary multilingual text, whitespace, and punctuation alone","duration":0.1860189999999875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bundle text sanitization"],"fullName":"bundle text sanitization the rendered bundle strips hostile controls from body, note, and quoted selection","status":"passed","title":"the rendered bundle strips hostile controls from body, note, and quoted selection","duration":1.2557030000000111,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573054645,"endTime":1791573054650.2556,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/bridge-core-sanitization.test.ts"},{"assertionResults":[{"ancestorTitles":["claude channel bridge"],"fullName":"claude channel bridge a dispatched bundle arrives as a channel notification and the tool closes the loop","status":"passed","title":"a dispatched bundle arrives as a channel notification and the tool closes the loop","duration":2875.840251,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573037527,"endTime":1791573040402.8403,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/claude-channel.test.ts"},{"assertionResults":[{"ancestorTitles":["file publication client"],"fullName":"file publication client publishes an ordinary file, a complete directory, and an optimistic new version","status":"passed","title":"publishes an ordinary file, a complete directory, and an optimistic new version","duration":176.40058399999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client classifies unsafe filesystem inputs before it creates an upload","status":"passed","title":"classifies unsafe filesystem inputs before it creates an upload","duration":3.090731999999889,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client fails closed for invalid credentials, invalid server origins, and cross-origin upload plans","status":"passed","title":"fails closed for invalid credentials, invalid server origins, and cross-origin upload plans","duration":11.597289000000046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client rejects incomplete and malformed plans and reports an unavailable server as protocol failures","status":"passed","title":"rejects incomplete and malformed plans and reports an unavailable server as protocol failures","duration":19.246666000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["file publication client"],"fullName":"file publication client uses deterministic media types and a safe binary fallback","status":"passed","title":"uses deterministic media types and a safe binary fallback","duration":0.23959499999989475,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572941581,"endTime":1791572941792.2466,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/file-publication-client.test.ts"},{"assertionResults":[{"ancestorTitles":["opencode bridge adapter"],"fullName":"opencode bridge adapter registers as kind opencode, holds delivery until a top-level session exists, injects through promptAsync, and beacons each work boundary","status":"passed","title":"registers as kind opencode, holds delivery until a top-level session exists, injects through promptAsync, and beacons each work boundary","duration":5200.548083000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["opencode bridge adapter"],"fullName":"opencode bridge adapter fails open when the plugin context lacks the expected surface: no registration, no crash, an honest tool error","status":"passed","title":"fails open when the plugin context lacks the expected surface: no registration, no crash, an honest tool error","duration":843.1146239999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["opencode bridge adapter"],"fullName":"opencode bridge adapter DSP-013-F: a session deleted during injection fails that dispatch, releases its comment, and the same bridge delivers the next dispatch","status":"passed","title":"DSP-013-F: a session deleted during injection fails that dispatch, releases its comment, and the same bridge delivers the next dispatch","duration":5133.538132000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572905255,"endTime":1791572916431.538,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/opencode-bridge.test.ts"},{"assertionResults":[{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks renders the recorded bundle template with note, paths, selections, and instruction","status":"passed","title":"renders the recorded bundle template with note, paths, selections, and instruction","duration":2.1032140000000084,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks omits the note line when absent and bounds long selections to the cap","status":"passed","title":"omits the note line when absent and bounds long selections to the cap","duration":0.460725999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks hostile slash-leading fields can never produce a slash-leading message","status":"passed","title":"hostile slash-leading fields can never produce a slash-leading message","duration":0.2707539999999824,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks connection keys are stable sha-256 digests of host and directory","status":"passed","title":"connection keys are stable sha-256 digests of host and directory","duration":1.4549209999999846,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks the display name prefers the override and falls back to the basename","status":"passed","title":"the display name prefers the override and falls back to the basename","duration":0.33810099999999466,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks environment configuration wins over local discovery and normalizes the origin","status":"passed","title":"environment configuration wins over local discovery and normalizes the origin","duration":11.46522299999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks local discovery resolves the loopback service record and token","status":"passed","title":"local discovery resolves the loopback service record and token","duration":1.983370999999977,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["pi bridge core building blocks"],"fullName":"pi bridge core building blocks no configuration, a non-loopback record, or a short token stays dormant","status":"passed","title":"no configuration, a non-loopback record, or a short token stays dormant","duration":1.8813420000000178,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573041492,"endTime":1791573041511.8813,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/pi-bridge-core.test.ts"},{"assertionResults":[{"ancestorTitles":["tool-result nudge composition"],"fullName":"tool-result nudge composition picks exactly one kind in priority order and nothing when nothing applies","status":"passed","title":"picks exactly one kind in priority order and nothing when nothing applies","duration":1.620283999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tool-result nudge composition"],"fullName":"tool-result nudge composition strips bidirectional and invisible characters from anything a nudge quotes","status":"passed","title":"strips bidirectional and invisible characters from anything a nudge quotes","duration":0.49338400000000604,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["tool-result nudge composition"],"fullName":"tool-result nudge composition bounds one block and appends it as a trailing text item only","status":"passed","title":"bounds one block and appends it as a trailing text item only","duration":1.1882930000000016,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573051990,"endTime":1791573051993.1882,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/tool-result-nudges.test.ts"},{"assertionResults":[{"ancestorTitles":["WebMCP mutation echo counts"],"fullName":"WebMCP mutation echo counts resolving the last open thread counts it resolved before the view commits","status":"passed","title":"resolving the last open thread counts it resolved before the view commits","duration":1.7050880000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["WebMCP mutation echo counts"],"fullName":"WebMCP mutation echo counts a thread the view has not loaded yet is counted from the fresh record","status":"passed","title":"a thread the view has not loaded yet is counted from the fresh record","duration":0.30798599999999965,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["WebMCP mutation echo counts"],"fullName":"WebMCP mutation echo counts an unchanged thread leaves the counts as the view has them","status":"passed","title":"an unchanged thread leaves the counts as the view has them","duration":0.21246299999999962,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573055128,"endTime":1791573055130.308,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/client/webmcp-counts.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact catalog comment queries"],"fullName":"artifact catalog comment queries ART-011-B ART-011-F: comment filters and ordering run before keyset pagination","status":"passed","title":"ART-011-B ART-011-F: comment filters and ordering run before keyset pagination","duration":232.90202799999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573047842,"endTime":1791573048074.902,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/artifact-catalog-comments.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact comparisons"],"fullName":"artifact comparisons AUTH-004-B DIF-001-B DIF-002-B DIF-002-F DIF-003-B DIF-003-F DIF-004-B DIF-004-F DIF-005-B DIF-005-F: public content exposes no history while authenticated comparisons work without Git","status":"passed","title":"AUTH-004-B DIF-001-B DIF-002-B DIF-002-F DIF-003-B DIF-003-F DIF-004-B DIF-004-F DIF-005-B DIF-005-F: public content exposes no history while authenticated comparisons work without Git","duration":319.8540260000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573010967,"endTime":1791573011286.854,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/artifact-comparison.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact lifecycle"],"fullName":"artifact lifecycle foundation: artifact and action lists use bounded stable cursors","status":"passed","title":"foundation: artifact and action lists use bounded stable cursors","duration":220.96197900000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifact lifecycle"],"fullName":"artifact lifecycle ART-006-B ART-006-F ART-007-B: deletion tombstones the artifact, blocks every read path, and retains committed versions","status":"passed","title":"ART-006-B ART-006-F ART-007-B: deletion tombstones the artifact, blocks every read path, and retains committed versions","duration":181.06956000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifact lifecycle"],"fullName":"artifact lifecycle foundation: a tombstone cannot commit without its action record","status":"passed","title":"foundation: a tombstone cannot commit without its action record","duration":60.165617999999995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572918399,"endTime":1791572918861.1655,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/artifact-lifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact and version management"],"fullName":"artifact and version management ART-002-B ART-003-B ART-004-B VER-001-B VER-001-F: authenticated metadata exposes immutable history and exact earlier bytes","status":"passed","title":"ART-002-B ART-003-B ART-004-B VER-001-B VER-001-F: authenticated metadata exposes immutable history and exact earlier bytes","duration":179.5145100000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["artifact and version management"],"fullName":"artifact and version management ART-005-B ART-005-F: restore and visibility changes are atomic, attributed, and fail closed","status":"passed","title":"ART-005-B ART-005-F: restore and visibility changes are atomic, attributed, and fail closed","duration":125.31043,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572925123,"endTime":1791572925428.3105,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/artifact-management.test.ts"},{"assertionResults":[{"ancestorTitles":["project-wide artifact search"],"fullName":"project-wide artifact search ART-009-B ART-009-F: search matches normalized name substrings and exact tags before pagination","status":"passed","title":"ART-009-B ART-009-F: search matches normalized name substrings and exact tags before pagination","duration":260.54564200000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573051184,"endTime":1791573051444.5457,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/artifact-search.test.ts"},{"assertionResults":[{"ancestorTitles":["artifact tags"],"fullName":"artifact tags ART-008-B ART-008-F: tags normalize, filter exactly, persist with the artifact, and reject invalid sets atomically","status":"passed","title":"ART-008-B ART-008-F: tags normalize, filter exactly, persist with the artifact, and reject invalid sets atomically","duration":310.21846800000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572975642,"endTime":1791572975952.2185,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/artifact-tags.test.ts"},{"assertionResults":[{"ancestorTitles":["managed API-key rotation"],"fullName":"managed API-key rotation rotation preserves human and service attribution while replacing each credential","status":"passed","title":"rotation preserves human and service attribution while replacing each credential","duration":188.93696699999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573028394,"endTime":1791573028582.937,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/auth-009-api-key-rotation.test.ts"},{"assertionResults":[{"ancestorTitles":["generic OIDC browser login"],"fullName":"generic OIDC browser login AUTH-019-B: a real OIDC round trip admits the bootstrap administrator and reuses the issuer-qualified binding","status":"passed","title":"AUTH-019-B: a real OIDC round trip admits the bootstrap administrator and reuses the issuer-qualified binding","duration":146.56165199999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC browser login"],"fullName":"generic OIDC browser login AUTH-019-F: unadmitted, disavowed, and email-less OIDC identities are refused without members or sessions","status":"passed","title":"AUTH-019-F: unadmitted, disavowed, and email-less OIDC identities are refused without members or sessions","duration":57.36682799999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572987455,"endTime":1791572987659.367,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/auth-019-oidc-login.test.ts"},{"assertionResults":[{"ancestorTitles":["OIDC id_token validation"],"fullName":"OIDC id_token validation AUTH-020-B: id_tokens skewed inside the thirty-second tolerance still complete login","status":"passed","title":"AUTH-020-B: id_tokens skewed inside the thirty-second tolerance still complete login","duration":181.3770740000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC id_token validation"],"fullName":"OIDC id_token validation AUTH-020-F: every token, state, and attempt defect is refused without a session, a member, or a leaked credential","status":"passed","title":"AUTH-020-F: every token, state, and attempt defect is refused without a session, a member, or a leaked credential","duration":114.38964600000008,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572959336,"endTime":1791572959631.3896,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/auth-020-oidc-validation.test.ts"},{"assertionResults":[{"ancestorTitles":["generic OIDC configuration"],"fullName":"generic OIDC configuration AUTH-021-B: a complete OIDC configuration serves browser login and no configured provider keeps keys working","status":"passed","title":"AUTH-021-B: a complete OIDC configuration serves browser login and no configured provider keeps keys working","duration":1978.7922629999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC configuration"],"fullName":"generic OIDC configuration AUTH-021-F: partial, doubled, and malformed OIDC configuration fails startup or login without weakening the server","status":"passed","title":"AUTH-021-F: partial, doubled, and malformed OIDC configuration fails startup or login without weakening the server","duration":5717.417090999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572950898,"endTime":1791572958593.417,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/auth-021-oidc-configuration.test.ts"},{"assertionResults":[{"ancestorTitles":["status-inclusive admission lookups"],"fullName":"status-inclusive admission lookups AUTH-029: email admission lookup returns active and inactive members","status":"passed","title":"AUTH-029: email admission lookup returns active and inactive members","duration":19.089809000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["status-inclusive admission lookups"],"fullName":"status-inclusive admission lookups AUTH-029: bound-subject lookup retains an inactive member after email changes","status":"passed","title":"AUTH-029: bound-subject lookup retains an inactive member after email changes","duration":2.4884320000000457,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["status-inclusive admission lookups"],"fullName":"status-inclusive admission lookups AUTH-029: email and bound-subject lookups preserve installation and issuer scopes","status":"passed","title":"AUTH-029: email and bound-subject lookups preserve installation and issuer scopes","duration":2.9283209999999826,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573047161,"endTime":1791573047185.9282,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/auth-029-identity-repository.test.ts"},{"assertionResults":[{"ancestorTitles":["authorization policy"],"fullName":"authorization policy AUTH-008-B AUTH-008-F: standalone mutations use membership and explicit capabilities","status":"passed","title":"AUTH-008-B AUTH-008-F: standalone mutations use membership and explicit capabilities","duration":9.120270000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authorization policy"],"fullName":"authorization policy foundation: another installation or an unscoped service cannot acquire authority","status":"passed","title":"foundation: another installation or an unscoped service cannot acquire authority","duration":1.684552999999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authorization policy"],"fullName":"authorization policy project discovery follows membership and explicit service authority","status":"passed","title":"project discovery follows membership and explicit service authority","duration":2.028336999999965,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573036762,"endTime":1791573036775.0283,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/authorization-policy.test.ts"},{"assertionResults":[{"ancestorTitles":["local immutable blob storage"],"fullName":"local immutable blob storage ART-004-F PUB-004-F: a conflicting stream cannot replace an existing fingerprint","status":"passed","title":"ART-004-F PUB-004-F: a conflicting stream cannot replace an existing fingerprint","duration":38.495664000000005,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573054818,"endTime":1791573054856.4956,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/blob-storage.test.ts"},{"assertionResults":[{"ancestorTitles":["extracted bridge core"],"fullName":"extracted bridge core BRP-001-B: the extracted core drives a fake host through register, claim, deliver, reply, and resolve against a real server","status":"passed","title":"BRP-001-B: the extracted core drives a fake host through register, claim, deliver, reply, and resolve against a real server","duration":4307.095541000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["extracted bridge core"],"fullName":"extracted bridge core BRP-001-F: the bridge core module graph reaches no Pi module and only host-neutral dependencies","status":"passed","title":"BRP-001-F: the bridge core module graph reaches no Pi module and only host-neutral dependencies","duration":30.21411799999987,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573005078,"endTime":1791573009415.214,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/brp-001-extracted-bridge-core.test.ts"},{"assertionResults":[{"ancestorTitles":["mailbox-tier dispatch inbox over MCP"],"fullName":"mailbox-tier dispatch inbox over MCP BRP-002-B: an MCP principal lists its inbox with implicit mailbox registration, claims the oldest dispatch as a rendered sanitized bundle, and reports delivered, and the agent listing shows the mailbox tier","status":"passed","title":"BRP-002-B: an MCP principal lists its inbox with implicit mailbox registration, claims the oldest dispatch as a rendered sanitized bundle, and reports delivered, and the agent listing shows the mailbox tier","duration":562.6580329999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["mailbox-tier dispatch inbox over MCP"],"fullName":"mailbox-tier dispatch inbox over MCP BRP-002-F: a second principal never sees the first principal's dispatches, claim with an empty inbox answers cleanly, and a delivered report for a dispatch the caller does not hold is refused","status":"passed","title":"BRP-002-F: a second principal never sees the first principal's dispatches, claim with an empty inbox answers cleanly, and a delivered report for a dispatch the caller does not hold is refused","duration":188.89626599999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572900881,"endTime":1791572901632.8962,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/brp-002-mailbox-inbox.test.ts"},{"assertionResults":[{"ancestorTitles":["comment threads pin one exact saved version"],"fullName":"comment threads pin one exact saved version CMT-001-B: a thread keeps naming its original artifact and version across later publishes and a restore","status":"passed","title":"CMT-001-B: a thread keeps naming its original artifact and version across later publishes and a restore","duration":193.98004200000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment threads pin one exact saved version"],"fullName":"comment threads pin one exact saved version CMT-001-F: publishing and restoring change nothing on an existing thread and a thread on an unknown version is refused","status":"passed","title":"CMT-001-F: publishing and restoring change nothing on an existing thread and a thread on an unknown version is refused","duration":85.15744399999994,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573012860,"endTime":1791573013139.1575,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-001-exact-version-thread.test.ts"},{"assertionResults":[{"ancestorTitles":["comment paths are validated against the version manifest"],"fullName":"comment paths are validated against the version manifest CMT-002-B: a thread names one manifest entry path inside its version and reads that path back","status":"passed","title":"CMT-002-B: a thread names one manifest entry path inside its version and reads that path back","duration":155.55122800000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment paths are validated against the version manifest"],"fullName":"comment paths are validated against the version manifest CMT-002-F: a path outside that version's manifest is refused as an invalid comment and stores nothing","status":"passed","title":"CMT-002-F: a path outside that version's manifest is refused as an invalid comment and stores nothing","duration":97.35389799999996,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573010073,"endTime":1791573010326.354,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-002-manifest-path.test.ts"},{"assertionResults":[{"ancestorTitles":["comment anchors stay opaque within their documented bounds"],"fullName":"comment anchors stay opaque within their documented bounds CMT-003-B: anchors of several shapes are stored and read back byte for byte","status":"passed","title":"CMT-003-B: anchors of several shapes are stored and read back byte for byte","duration":233.00391000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment anchors stay opaque within their documented bounds"],"fullName":"comment anchors stay opaque within their documented bounds CMT-003-F: oversize anchors and out-of-range points are refused while a valid anchor stays unchanged","status":"passed","title":"CMT-003-F: oversize anchors and out-of-range points are refused while a valid anchor stays unchanged","duration":92.99429700000007,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572977241,"endTime":1791572977566.9944,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-003-anchor-opacity.test.ts"},{"assertionResults":[{"ancestorTitles":["comment replies are one level deep and open-thread only"],"fullName":"comment replies are one level deep and open-thread only CMT-004-B: an open thread collects replies that carry no replies of their own","status":"passed","title":"CMT-004-B: an open thread collects replies that carry no replies of their own","duration":206.63746500000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment replies are one level deep and open-thread only"],"fullName":"comment replies are one level deep and open-thread only CMT-004-F: a resolved thread refuses replies and a concurrent resolve never leaves a reply on a resolved thread","status":"passed","title":"CMT-004-F: a resolved thread refuses replies and a concurrent resolve never leaves a reply on a resolved thread","duration":334.4960429999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572967599,"endTime":1791572968139.496,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-004-reply-rules.test.ts"},{"assertionResults":[{"ancestorTitles":["resolving and reopening one comment thread"],"fullName":"resolving and reopening one comment thread CMT-005-B: one state change resolves and reopens a thread and records who changed it and when","status":"passed","title":"CMT-005-B: one state change resolves and reopens a thread and records who changed it and when","duration":153.68010700000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["resolving and reopening one comment thread"],"fullName":"resolving and reopening one comment thread CMT-005-F: an unknown state value is refused and a resolved thread stays resolved on every read path","status":"passed","title":"CMT-005-F: an unknown state value is refused and a resolved thread stays resolved on every read path","duration":82.82963500000005,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572988350,"endTime":1791572988586.8296,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-005-state-transitions.test.ts"},{"assertionResults":[{"ancestorTitles":["comment read authority"],"fullName":"comment read authority CMT-006-B: an admitted member reads threads and replies on the application origin","status":"passed","title":"CMT-006-B: an admitted member reads threads and replies on the application origin","duration":194.506079,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment read authority"],"fullName":"comment read authority CMT-006-F: public-link viewers, anonymous requests, and the content origin never see comments","status":"passed","title":"CMT-006-F: public-link viewers, anonymous requests, and the content origin never see comments","duration":66.17770900000005,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573027439,"endTime":1791573027700.1777,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-006-comment-read-authority.test.ts"},{"assertionResults":[{"ancestorTitles":["comment write capability"],"fullName":"comment write capability CMT-007-B: a direct human member and a comment-write key open, answer, resolve, and reopen threads","status":"passed","title":"CMT-007-B: a direct human member and a comment-write key open, answer, resolve, and reopen threads","duration":255.0655710000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment write capability"],"fullName":"comment write capability CMT-007-F: a key holding only artifact read authority is denied every comment mutation","status":"passed","title":"CMT-007-F: a key holding only artifact read authority is denied every comment mutation","duration":84.96418900000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572968847,"endTime":1791572969186.964,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-007-comment-write-capability.test.ts"},{"assertionResults":[{"ancestorTitles":["comment authorship"],"fullName":"comment authorship CMT-008-B: authors edit and delete their own comments while an administrator deletes another member's thread","status":"passed","title":"CMT-008-B: authors edit and delete their own comments while an administrator deletes another member's thread","duration":243.18437899999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment authorship"],"fullName":"comment authorship CMT-008-F: another member and an administrator cannot rewrite an author's comment while the administrator's delete is recorded","status":"passed","title":"CMT-008-F: another member and an administrator cannot rewrite an author's comment while the administrator's delete is recorded","duration":74.60259199999996,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572947649,"endTime":1791572947966.6025,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-008-comment-authorship.test.ts"},{"assertionResults":[{"ancestorTitles":["comment project scope"],"fullName":"comment project scope CMT-009-B: every comment operation runs inside its own project and returns project-scoped records","status":"passed","title":"CMT-009-B: every comment operation runs inside its own project and returns project-scoped records","duration":214.468887,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment project scope"],"fullName":"comment project scope CMT-009-F: identifiers presented under another project or installation are not found without disclosure","status":"passed","title":"CMT-009-F: identifiers presented under another project or installation are not found without disclosure","duration":181.073984,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572919519,"endTime":1791572919915.074,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-009-comment-project-scope.test.ts"},{"assertionResults":[{"ancestorTitles":["comment create idempotency"],"fullName":"comment create idempotency CMT-010-B: repeated thread and reply creates with one key replay the original records","status":"passed","title":"CMT-010-B: repeated thread and reply creates with one key replay the original records","duration":171.47160700000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment create idempotency"],"fullName":"comment create idempotency CMT-010-F: one key with different input conflicts and writes no duplicate comment","status":"passed","title":"CMT-010-F: one key with different input conflicts and writes no duplicate comment","duration":119.08425499999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573011921,"endTime":1791573012212.0842,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-010-comment-idempotency.test.ts"},{"assertionResults":[{"ancestorTitles":["comment mutations and the action ledger"],"fullName":"comment mutations and the action ledger CMT-011-B: every comment mutation appends an attributed action naming the artifact and version","status":"passed","title":"CMT-011-B: every comment mutation appends an attributed action naming the artifact and version","duration":259.124821,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment mutations and the action ledger"],"fullName":"comment mutations and the action ledger CMT-011-F: a comment mutation cannot commit when its action record is refused","status":"passed","title":"CMT-011-F: a comment mutation cannot commit when its action record is refused","duration":90.17011999999988,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572904088,"endTime":1791572904437.1702,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-011-comment-action-ledger.test.ts"},{"assertionResults":[{"ancestorTitles":["comment surface parity and change polling"],"fullName":"comment surface parity and change polling CMT-012-B: HTTP and MCP drive the same comment operations and page changes with a since time and a cursor","status":"passed","title":"CMT-012-B: HTTP and MCP drive the same comment operations and page changes with a since time and a cursor","duration":450.69216399999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment surface parity and change polling"],"fullName":"comment surface parity and change polling CMT-012-F: MCP and HTTP agree for one principal and a since poll resuming from the previous pass start misses no change","status":"passed","title":"CMT-012-F: MCP and HTTP agree for one principal and a since poll resuming from the previous pass start misses no change","duration":251.58465899999987,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572897247,"endTime":1791572897949.5847,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-012-comment-surface-parity.test.ts"},{"assertionResults":[{"ancestorTitles":["version file route for the review viewer"],"fullName":"version file route for the review viewer CMT-013-B: an artifact-read principal reads exact manifest bytes with non-renderable headers","status":"passed","title":"CMT-013-B: an artifact-read principal reads exact manifest bytes with non-renderable headers","duration":165.174889,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["version file route for the review viewer"],"fullName":"version file route for the review viewer foundation: version file responses revalidate and serve byte ranges","status":"passed","title":"foundation: version file responses revalidate and serve byte ranges","duration":80.45099899999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["version file route for the review viewer"],"fullName":"version file route for the review viewer CMT-013-F: unauthenticated, public-link, and read-incapable requests get no manifest bytes","status":"passed","title":"CMT-013-F: unauthenticated, public-link, and read-incapable requests get no manifest bytes","duration":67.54560300000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572939545,"endTime":1791572939858.5457,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-013-version-file-route.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatched comment deletion"],"fullName":"dispatched comment deletion CMT-020-B: queued, claimed, and delivered dispatches hold comments while addressed, failed, and canceled dispatches release them","status":"passed","title":"CMT-020-B: queued, claimed, and delivered dispatches hold comments while addressed, failed, and canceled dispatches release them","duration":284.94487000000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatched comment deletion"],"fullName":"dispatched comment deletion CMT-020-F: a concurrent send and delete commits only one complete SQLite outcome","status":"passed","title":"CMT-020-F: a concurrent send and delete commits only one complete SQLite outcome","duration":46.50731200000007,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573036145,"endTime":1791573036476.5073,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-020-dispatched-comment-deletion.test.ts"},{"assertionResults":[{"ancestorTitles":["complete immutable version downloads"],"fullName":"complete immutable version downloads CMT-021-B: a permitted user downloads the selected version as a deterministic path-preserving ZIP","status":"passed","title":"CMT-021-B: a permitted user downloads the selected version as a deterministic path-preserving ZIP","duration":193.719202,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete immutable version downloads"],"fullName":"complete immutable version downloads CMT-021-F: unauthorized and mismatched version requests disclose no artifact bytes","status":"passed","title":"CMT-021-F: unauthorized and mismatched version requests disclose no artifact bytes","duration":62.26891999999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573029247,"endTime":1791573029503.2688,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/cmt-021-version-download.test.ts"},{"assertionResults":[{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing MAN-006-B CNT-005-B: a canonical complete site serves every declared file from one exact version origin","status":"passed","title":"MAN-006-B CNT-005-B: a canonical complete site serves every declared file from one exact version origin","duration":289.4407829999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing foundation: a configured public origin owns every application URL behind a TLS-terminating proxy","status":"passed","title":"foundation: a configured public origin owns every application URL behind a TLS-terminating proxy","duration":34.85769200000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing PUB-001-B PUB-003-B: a staged site resumes after restart and commits only after every file is verified","status":"passed","title":"PUB-001-B PUB-003-B: a staged site resumes after restart and commits only after every file is verified","duration":68.06336799999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing MAN-005-F: absent entries, duplicate paths, and portable-name collisions are rejected before upload creation","status":"passed","title":"MAN-005-F: absent entries, duplicate paths, and portable-name collisions are rejected before upload creation","duration":44.97788999999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing CNT-007-B CNT-007-F: SPA fallback is explicit, navigation-only, and never replaces an exact file","status":"passed","title":"CNT-007-B CNT-007-F: SPA fallback is explicit, navigation-only, and never replaces an exact file","duration":69.89551699999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing immutable files support one bounded byte range without buffering the full object","status":"passed","title":"immutable files support one bounded byte range without buffering the full object","duration":60.928633999999875,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing CNT-002-B CNT-002-F: ordinary files use browser-native or download-safe responses","status":"passed","title":"CNT-002-B CNT-002-F: ordinary files use browser-native or download-safe responses","duration":176.91357599999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing foundation: SPA routing rejects a non-HTML entry before allocating upload slots","status":"passed","title":"foundation: SPA routing rejects a non-HTML entry before allocating upload slots","duration":29.6837230000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing foundation: HTTP upload locations reject invalid credentials and another installation","status":"passed","title":"foundation: HTTP upload locations reject invalid credentials and another installation","duration":70.09324399999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["complete-site publishing"],"fullName":"complete-site publishing PUB-003-F: missing, truncated, oversized, and fingerprint-mismatched staged files cannot commit","status":"passed","title":"PUB-003-F: missing, truncated, oversized, and fingerprint-mismatched staged files cannot commit","duration":40.395970000000034,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572827548,"endTime":1791572828434.396,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/complete-site.test.ts"},{"assertionResults":[{"ancestorTitles":["content access lifecycle"],"fullName":"content access lifecycle foundation: bootstrap and content-session expiration fail closed","status":"passed","title":"foundation: bootstrap and content-session expiration fail closed","duration":75.26164299999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["content access lifecycle"],"fullName":"content access lifecycle bootstrap replay, tampering, and host substitution do not grant a session","status":"passed","title":"bootstrap replay, tampering, and host substitution do not grant a session","duration":24.85981800000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["content access lifecycle"],"fullName":"content access lifecycle preview lease foundation: tampered leases fail and valid leases expire at their exact boundary","status":"passed","title":"preview lease foundation: tampered leases fail and valid leases expire at their exact boundary","duration":22.959821000000034,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572976389,"endTime":1791572976511.9597,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/content-access-lifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["registered agent registry"],"fullName":"registered agent registry DSP-001-B: one connection key upserts one self-named agent whose connected state follows its claim polling","status":"passed","title":"DSP-001-B: one connection key upserts one self-named agent whose connected state follows its claim polling","duration":3192.840295,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["registered agent registry"],"fullName":"registered agent registry DSP-001-F: reaping a stale agent row leaves its dispatch attributed, and another installation's agent never appears","status":"passed","title":"DSP-001-F: reaping a stale agent row leaves its dispatch attributed, and another installation's agent never appears","duration":165.18396499999972,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572943667,"endTime":1791572947025.184,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-001-agent-registry.test.ts"},{"assertionResults":[{"ancestorTitles":["agent connection authority"],"fullName":"agent connection authority DSP-002-B: an agent-connect key and the local all-capability token register, claim, and report delivered and failed","status":"passed","title":"DSP-002-B: an agent-connect key and the local all-capability token register, claim, and report delivered and failed","duration":305.621802,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent connection authority"],"fullName":"agent connection authority DSP-002-F: a read-only key and a direct human are refused every agent operation, and a report from a non-holder is a state conflict","status":"passed","title":"DSP-002-F: a read-only key and a direct human are refused every agent operation, and a report from a non-holder is a state conflict","duration":103.00325900000007,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573015024,"endTime":1791573015433.0032,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-002-agent-connect-authority.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch send"],"fullName":"agent dispatch send DSP-003-B: one send becomes one ordered dispatch, and the same idempotency key replays the original","status":"passed","title":"DSP-003-B: one send becomes one ordered dispatch, and the same idempotency key replays the original","duration":551.536322,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch send"],"fullName":"agent dispatch send DSP-003-F: an empty, oversized, cross-project, resolved, or already dispatched bundle is rejected without marking a thread","status":"passed","title":"DSP-003-F: an empty, oversized, cross-project, resolved, or already dispatched bundle is rejected without marking a thread","duration":108.90780900000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572990122,"endTime":1791572990781.9077,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-003-dispatch-send.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatched thread visibility"],"fullName":"dispatched thread visibility DSP-004-B: HTTP, MCP, and the web application hide a sent bundle by default and answer the same for every dispatched filter","status":"passed","title":"DSP-004-B: HTTP, MCP, and the web application hide a sent bundle by default and answer the same for every dispatched filter","duration":353.431378,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatched thread visibility"],"fullName":"dispatched thread visibility DSP-004-F: no default listing on any surface can show a dispatched thread, and the filter never reaches another project or installation","status":"passed","title":"DSP-004-F: no default listing on any surface can show a dispatched thread, and the filter never reaches another project or installation","duration":218.15961900000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572942443,"endTime":1791572943015.1597,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-004-dispatched-thread-visibility.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatch claim order"],"fullName":"dispatch claim order DSP-005-B: three queued bundles are claimed oldest first, one at a time, and a waiting poll answers inside its bound","status":"passed","title":"DSP-005-B: three queued bundles are claimed oldest first, one at a time, and a waiting poll answers inside its bound","duration":2239.1123000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatch claim order"],"fullName":"dispatch claim order DSP-005-F: a second claim while one is held answers empty, and simultaneous claims never hand one bundle to two agents","status":"passed","title":"DSP-005-F: a second claim while one is held answers empty, and simultaneous claims never hand one bundle to two agents","duration":2076.8374639999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573031169,"endTime":1791573035485.8374,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-005-claim-order.test.ts"},{"assertionResults":[{"ancestorTitles":["dispatch claim lease"],"fullName":"dispatch claim lease DSP-006-B: an expired lease returns the bundle to the queue and the restarted agent claims it again","status":"passed","title":"DSP-006-B: an expired lease returns the bundle to the queue and the restarted agent claims it again","duration":239.11438699999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["dispatch claim lease"],"fullName":"dispatch claim lease DSP-006-F: a claimer that returns after its lease expired cannot report, and a reclaim never leaves two claims active","status":"passed","title":"DSP-006-F: a claimer that returns after its lease expired cannot report, and a reclaim never leaves two claims active","duration":77.66984200000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573030163,"endTime":1791573030480.67,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-006-claim-lease.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch state machine"],"fullName":"agent dispatch state machine DSP-007-B: one dispatch walks queued, claimed, delivered, and addressed with its own stamp on every transition","status":"passed","title":"DSP-007-B: one dispatch walks queued, claimed, delivered, and addressed with its own stamp on every transition","duration":217.63591299999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch state machine"],"fullName":"agent dispatch state machine DSP-007-F: every transition outside the machine is a state conflict that moves no recorded time","status":"passed","title":"DSP-007-F: every transition outside the machine is a state conflict that moves no recorded time","duration":222.49149199999988,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572902337,"endTime":1791572902777.4915,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-007-dispatch-state-machine.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch addressed inference"],"fullName":"agent dispatch addressed inference DSP-008-B: a delivered bundle becomes addressed once every thread resolves, stamped when the read first observes it","status":"passed","title":"DSP-008-B: a delivered bundle becomes addressed once every thread resolves, stamped when the read first observes it","duration":263.29810699999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch addressed inference"],"fullName":"agent dispatch addressed inference DSP-008-F: an unresolved thread holds the dispatch delivered, and reopening one afterwards neither undoes nor restamps addressed","status":"passed","title":"DSP-008-F: an unresolved thread holds the dispatch delivered, and reopening one afterwards neither undoes nor restamps addressed","duration":93.26503500000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572921562,"endTime":1791572921919.2651,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-008-addressed-inference.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch thread release"],"fullName":"agent dispatch thread release DSP-009-B: failure, cancellation, and an agent that stops polling each release their threads back into the default listings","status":"passed","title":"DSP-009-B: failure, cancellation, and an agent that stops polling each release their threads back into the default listings","duration":266.586764,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch thread release"],"fullName":"agent dispatch thread release DSP-009-F: an addressed dispatch releases nothing and a held thread joins no second dispatch until it is released","status":"passed","title":"DSP-009-F: an addressed dispatch releases nothing and a held thread joins no second dispatch until it is released","duration":144.2546840000001,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572898669,"endTime":1791572899080.2546,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-009-dispatch-release.test.ts"},{"assertionResults":[{"ancestorTitles":["agent dispatch authorization"],"fullName":"agent dispatch authorization DSP-010-B: a human member and a manage-any key send and cancel while read authority lists and reads dispatches","status":"passed","title":"DSP-010-B: a human member and a manage-any key send and cancel while read authority lists and reads dispatches","duration":293.11475299999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch authorization"],"fullName":"agent dispatch authorization a foreign agent-connect key cannot claim or report for an agent it did not register","status":"passed","title":"a foreign agent-connect key cannot claim or report for an agent it did not register","duration":81.24094400000013,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent dispatch authorization"],"fullName":"agent dispatch authorization DSP-010-F: an agent-connect key can neither send nor cancel, and identifiers from another project or installation are not found without disclosure","status":"passed","title":"DSP-010-F: an agent-connect key can neither send nor cancel, and identifiers from another project or installation are not found without disclosure","duration":156.49498700000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572858274,"endTime":1791572858805.4949,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-010-dispatch-authorization.test.ts"},{"assertionResults":[{"ancestorTitles":["bridge bundle rendering"],"fullName":"bridge bundle rendering DSP-011-B: a bundle renders as one message with sender line, note, numbered items, quoted selection, bodies, thread ids, and the reply-and-resolve instruction, delivered as follow-up","status":"passed","title":"DSP-011-B: a bundle renders as one message with sender line, note, numbered items, quoted selection, bodies, thread ids, and the reply-and-resolve instruction, delivered as follow-up","duration":4302.253873,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge bundle rendering"],"fullName":"bridge bundle rendering DSP-011-F: hostile bundle text never yields a leading slash and every delivery names follow-up, never steering","status":"passed","title":"DSP-011-F: hostile bundle text never yields a leading slash and every delivery names follow-up, never steering","duration":1103.8329789999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573019407,"endTime":1791573024812.833,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-011-bridge-bundle-render.test.ts"},{"assertionResults":[{"ancestorTitles":["bridge fail-open discipline"],"fullName":"bridge fail-open discipline DSP-012-B: without configuration one notice and no polling; configured, it registers, claims, holds through compaction, reports delivery, and shuts down cleanly","status":"passed","title":"DSP-012-B: without configuration one notice and no polling; configured, it registers, claims, holds through compaction, reports delivery, and shuts down cleanly","duration":3836.102554,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge fail-open discipline"],"fullName":"bridge fail-open discipline DSP-013-B: delivery waits for host admission, an asynchronous refusal fails only that dispatch, and the loop accepts the next bundle","status":"passed","title":"DSP-013-B: delivery waits for host admission, an asynchronous refusal fails only that dispatch, and the loop accepts the next bundle","duration":5236.323958000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bridge fail-open discipline"],"fullName":"bridge fail-open discipline DSP-012-F: an unreachable backend backs off within bounds and a stale host handle ends the loop, with no error reaching the host","status":"passed","title":"DSP-012-F: an unreachable backend backs off within bounds and a stale host handle ends the loop, with no error reaching the host","duration":3199.8667960000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572926628,"endTime":1791572938900.8667,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/dsp-012-bridge-fail-open.test.ts"},{"assertionResults":[{"ancestorTitles":["Git history capability monitoring"],"fullName":"Git history capability monitoring Git monitor lifecycle: a provider outage recovers and shutdown stops later checks","status":"passed","title":"Git monitor lifecycle: a provider outage recovers and shutdown stops later checks","duration":19.729408999999976,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573054306,"endTime":1791573054325.7295,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/git-history-capability-monitor.test.ts"},{"assertionResults":[{"ancestorTitles":["optional Git handoff configuration and discovery"],"fullName":"optional Git handoff configuration and discovery GIT-010 GIT-012 GIT-014 foundation: absent and off configurations expose bounded disabled defaults without reading provider companions","status":"passed","title":"GIT-010 GIT-012 GIT-014 foundation: absent and off configurations expose bounded disabled defaults without reading provider companions","duration":5.5219509999999445,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["optional Git handoff configuration and discovery"],"fullName":"optional Git handoff configuration and discovery GIT-010 GIT-012 foundation: complete Node REST configuration is parsed, while hostile or malformed families degrade without exposing secrets","status":"passed","title":"GIT-010 GIT-012 foundation: complete Node REST configuration is parsed, while hostile or malformed families degrade without exposing secrets","duration":8.50794499999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["optional Git handoff configuration and discovery"],"fullName":"optional Git handoff configuration and discovery GIT-010 GIT-013 foundation: a successful read-only check persists identity, reports live parity, and rejects a changed destination before another provider call","status":"passed","title":"GIT-010 GIT-013 foundation: a successful read-only check persists identity, reports live parity, and rejects a changed destination before another provider call","duration":266.782329,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572899779,"endTime":1791572900059.7822,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/git-history-discovery.test.ts"},{"assertionResults":[{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-008-B GIT-012-B: an explicitly enabled project backfills while every other project stays off","status":"passed","title":"GIT-008-B GIT-012-B: an explicitly enabled project backfills while every other project stays off","duration":8105.650238,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting foundation: an unconfigured deployment cannot estimate or enable, and the default remains off","status":"passed","title":"foundation: an unconfigured deployment cannot estimate or enable, and the default remains off","duration":28.552074000001085,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-009-F: retrying an already-enabled setting succeeds during a provider outage","status":"passed","title":"GIT-009-F: retrying an already-enabled setting succeeds during a provider outage","duration":51.31612200000018,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-002-F GIT-003-F GIT-008-F: a failed mirror resumes after process restart without a duplicate commit","status":"passed","title":"GIT-002-F GIT-003-F GIT-008-F: a failed mirror resumes after process restart without a duplicate commit","duration":10267.146272,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-005-F GIT-009-B: clone access is authorized and bounded, disablement preserves the repository, and artifact deletion removes it","status":"passed","title":"GIT-005-F GIT-009-B: clone access is authorized and bounded, disablement preserves the repository, and artifact deletion removes it","duration":4052.524713999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-004-B GIT-004-F: copy limits produce deterministic files and pointers across retry","status":"passed","title":"GIT-004-B GIT-004-F: copy limits produce deterministic files and pointers across retry","duration":4046.9921329999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-014-B GIT-014-F: a logical budget pauses only Git and resumes after an explicit budget increase","status":"passed","title":"GIT-014-B GIT-014-F: a logical budget pauses only Git and resumes after an explicit budget increase","duration":4115.838813999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["simple per-project Git history setting"],"fullName":"simple per-project Git history setting GIT-011-B GIT-011-F: purge plans without provider access and resumes after partial deletion","status":"passed","title":"GIT-011-B GIT-011-F: purge plans without provider access and resumes after partial deletion","duration":2078.8354510000026,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572766304,"endTime":1791572799050.8354,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/git-history-project-setting.test.ts"},{"assertionResults":[{"ancestorTitles":["Cloudflare Artifacts REST availability"],"fullName":"Cloudflare Artifacts REST availability GIT-010 GIT-013 foundation: the read-only namespace check is path-scoped, authenticated, bounded, and safely classified","status":"passed","title":"GIT-010 GIT-013 foundation: the read-only namespace check is path-scoped, authenticated, bounded, and safely classified","duration":54.92498800000004,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573051802,"endTime":1791573051856.925,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/git-history-provider-health.test.ts"},{"assertionResults":[{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-024-F AUTH-027-B AUTH-027-F AUTH-028-F: bootstrap membership and managed keys fail closed","status":"passed","title":"AUTH-024-F AUTH-027-B AUTH-027-F AUTH-028-F: bootstrap membership and managed keys fail closed","duration":235.18518100000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-023-B AUTH-023-F AUTH-024-B: local-owner browsers receive a stable session only across the loopback same-origin boundary","status":"passed","title":"AUTH-023-B AUTH-023-F AUTH-024-B: local-owner browsers receive a stable session only across the loopback same-origin boundary","duration":51.7969609999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-025-B AUTH-025-F AUTH-026-F: private-team mode advertises its provider and has no local browser bootstrap route","status":"passed","title":"AUTH-025-B AUTH-025-F AUTH-026-F: private-team mode advertises its provider and has no local browser bootstrap route","duration":33.84723599999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access concurrent private-team startup cannot seed two machine authorities","status":"passed","title":"concurrent private-team startup cannot seed two machine authorities","duration":30.46147600000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-010-B AUTH-010-F AUTH-011-B AUTH-011-F AUTH-012-B AUTH-012-F: browser credentials stay on the application host and mutations require same-origin proof","status":"passed","title":"AUTH-010-B AUTH-010-F AUTH-011-B AUTH-011-F AUTH-012-B AUTH-012-F: browser credentials stay on the application host and mutations require same-origin proof","duration":31.98041599999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-022-B AUTH-022-F: session authentication caching stays a bounded-staleness optimization","status":"passed","title":"AUTH-022-B AUTH-022-F: session authentication caching stays a bounded-staleness optimization","duration":49.29196000000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access AUTH-001-B AUTH-001-F AUTH-028-B: external login admits only the configured first administrator and consumes state once","status":"passed","title":"AUTH-001-B AUTH-001-F AUTH-028-B: external login admits only the configured first administrator and consumes state once","duration":74.134049,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access external login reports a malformed verified identity as a typed conflict","status":"passed","title":"external login reports a malformed verified identity as a typed conflict","duration":35.00565400000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access a configured external bearer verifier shares the principal boundary without managed-key parser fallback","status":"passed","title":"a configured external bearer verifier shares the principal boundary without managed-key parser fallback","duration":41.43503899999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["installation identity and access"],"fullName":"installation identity and access API OAuth metadata and challenge name the exact API resource without enabling it by default","status":"passed","title":"API OAuth metadata and challenge name the exact API resource without enabling it by default","duration":32.96306500000014,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572706500,"endTime":1791572707115.9631,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/installation-identity.test.ts"},{"assertionResults":[{"ancestorTitles":["linked artifacts are off by default and reference in place"],"fullName":"linked artifacts are off by default and reference in place LNK-001-B: an enabled local deployment advertises the capability, links one file in place over HTTP and MCP, and never absorbs an unlinked file","status":"passed","title":"LNK-001-B: an enabled local deployment advertises the capability, links one file in place over HTTP and MCP, and never absorbs an unlinked file","duration":318.172362,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked artifacts are off by default and reference in place"],"fullName":"linked artifacts are off by default and reference in place LNK-001-F: with the feature off the capability is absent, every linked route and tool answers the stable shape, and no file becomes an artifact","status":"passed","title":"LNK-001-F: with the feature off the capability is absent, every linked route and tool answers the stable shape, and no file becomes an artifact","duration":149.36681599999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573013829,"endTime":1791573014296.3667,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-001-off-by-default.test.ts"},{"assertionResults":[{"ancestorTitles":["linking a file publishes an ordinary first version"],"fullName":"linking a file publishes an ordinary first version LNK-002-B: one linked regular file becomes an artifact whose first version carries its verified bytes, manifest, and action","status":"passed","title":"LNK-002-B: one linked regular file becomes an artifact whose first version carries its verified bytes, manifest, and action","duration":160.49475300000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linking a file publishes an ordinary first version"],"fullName":"linking a file publishes an ordinary first version LNK-002-F: a path that is not a readable regular file creates nothing, and replaying one link key returns the original artifact","status":"passed","title":"LNK-002-F: a path that is not a readable regular file creates nothing, and replaying one link key returns the original artifact","duration":113.759814,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572940577,"endTime":1791572940851.7598,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-002-link-initial-capture.test.ts"},{"assertionResults":[{"ancestorTitles":["linked source freshness on ordinary metadata reads"],"fullName":"linked source freshness on ordinary metadata reads LNK-003-B: external modification, replacement, and removal move freshness while the version list and delivered bytes hold still","status":"passed","title":"LNK-003-B: external modification, replacement, and removal move freshness while the version list and delivered bytes hold still","duration":220.612706,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source freshness on ordinary metadata reads"],"fullName":"linked source freshness on ordinary metadata reads LNK-003-F: a refresh cannot write a version or an action, and a failed stat degrades only the freshness field","status":"passed","title":"LNK-003-F: a refresh cannot write a version or an action, and a failed stat degrades only the freshness field","duration":98.030125,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572974451,"endTime":1791572974770.03,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-003-binding-freshness.test.ts"},{"assertionResults":[{"ancestorTitles":["capturing a linked source as a new immutable version"],"fullName":"capturing a linked source as a new immutable version LNK-004-B: capture publishes the current source bytes as a new version and an in-sync capture replays the current one","status":"passed","title":"LNK-004-B: capture publishes the current source bytes as a new version and an in-sync capture replays the current one","duration":198.71831499999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["capturing a linked source as a new immutable version"],"fullName":"capturing a linked source as a new immutable version LNK-004-F: a stale expected current version returns the publish conflict with the actual current version and no version is written","status":"passed","title":"LNK-004-F: a stale expected current version returns the publish conflict with the actual current version and no version is written","duration":128.917869,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572962494,"endTime":1791572962821.918,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-004-source-capture.test.ts"},{"assertionResults":[{"ancestorTitles":["relinking one binding at a moved source file"],"fullName":"relinking one binding at a moved source file LNK-005-B: a moved file relinks under its expected hash and keeps the artifact, versions, and comment threads","status":"passed","title":"LNK-005-B: a moved file relinks under its expected hash and keeps the artifact, versions, and comment threads","duration":211.88478599999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["relinking one binding at a moved source file"],"fullName":"relinking one binding at a moved source file LNK-005-F: a mismatched hash, a path outside every root, and a path inside the data directory all change nothing","status":"passed","title":"LNK-005-F: a mismatched hash, a path outside every root, and a path inside the data directory all change nothing","duration":91.75927400000012,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572961474,"endTime":1791572961777.7593,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-005-relink-moved-source.test.ts"},{"assertionResults":[{"ancestorTitles":["linked source paths are canonicalized, root-checked, and self-protected"],"fullName":"linked source paths are canonicalized, root-checked, and self-protected LNK-006-B: an admitted member links inside a root over loopback and reads the path only through authenticated metadata","status":"passed","title":"LNK-006-B: an admitted member links inside a root over loopback and reads the path only through authenticated metadata","duration":141.35033299999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source paths are canonicalized, root-checked, and self-protected"],"fullName":"linked source paths are canonicalized, root-checked, and self-protected LNK-006-F: every escape, protected target, unauthorized caller, and non-loopback origin is refused without disclosing a path","status":"passed","title":"LNK-006-F: every escape, protected target, unauthorized caller, and non-loopback origin is refused without disclosing a path","duration":64.77150499999993,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573025460,"endTime":1791573025666.7715,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-006-path-safety.test.ts"},{"assertionResults":[{"ancestorTitles":["the live view reaches only authenticated local members"],"fullName":"the live view reaches only authenticated local members LNK-007-B: a local member streams the live bytes no-store while captured versions stay immutably cached","status":"passed","title":"LNK-007-B: a local member streams the live bytes no-store while captured versions stay immutably cached","duration":227.52176599999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["the live view reaches only authenticated local members"],"fullName":"the live view reaches only authenticated local members LNK-007-F: anonymous, cross-origin, public-link, and non-member readers never receive a live byte, and a missing source degrades to the capture","status":"passed","title":"LNK-007-F: anonymous, cross-origin, public-link, and non-member readers never receive a live byte, and a missing source degrades to the capture","duration":72.78808400000003,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573017229,"endTime":1791573017529.788,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-007-live-view-audience.test.ts"},{"assertionResults":[{"ancestorTitles":["comment threads on a linked artifact anchor to captured versions"],"fullName":"comment threads on a linked artifact anchor to captured versions LNK-008-B: an in-sync thread anchors to the current version and a drifted one implicitly captures for the commenting principal","status":"passed","title":"LNK-008-B: an in-sync thread anchors to the current version and a drifted one implicitly captures for the commenting principal","duration":224.00871099999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["comment threads on a linked artifact anchor to captured versions"],"fullName":"comment threads on a linked artifact anchor to captured versions LNK-008-F: an aborted implicit capture fails the whole comment request and leaves no thread and no version","status":"passed","title":"LNK-008-F: an aborted implicit capture fails the whole comment request and leaves no thread and no version","duration":88.16770100000008,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572917451,"endTime":1791572917763.1677,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/lnk-008-comment-implicit-capture.test.ts"},{"assertionResults":[{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP MCP-001-B MCP-001-F MCP-002-B MCP-002-F MCP-003-B MCP-003-F MCP-004-B MCP-004-F MCP-005-B MCP-005-F: modern discovery is authenticated, stateless, and exact","status":"passed","title":"MCP-001-B MCP-001-F MCP-002-B MCP-002-F MCP-003-B MCP-003-F MCP-004-B MCP-004-F MCP-005-B MCP-005-F: modern discovery is authenticated, stateless, and exact","duration":831.412231,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP MCP-006-B MCP-006-F MCP-007-B MCP-007-F MCP-008-B MCP-008-F MCP-015-B MCP-015-F PUB-013-B PUB-013-F: every advertised artifact operation runs over real files and shared policy","status":"passed","title":"MCP-006-B MCP-006-F MCP-007-B MCP-007-F MCP-008-B MCP-008-F MCP-015-B MCP-015-F PUB-013-B PUB-013-F: every advertised artifact operation runs over real files and shared policy","duration":494.42512499999975,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP MCP manages projects and refuses ambiguous artifact scope","status":"passed","title":"MCP manages projects and refuses ambiguous artifact scope","duration":110.63995099999966,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP external credentials use the same installation and capability policy as HTTP","status":"passed","title":"external credentials use the same installation and capability policy as HTTP","duration":92.7513640000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP the linked-file tools answer the stable coded error while the capability is off","status":"passed","title":"the linked-file tools answer the stable coded error while the capability is off","duration":72.9645889999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["modern MCP HTTP"],"fullName":"modern MCP HTTP the linked-file tools link, capture, and relink one file on the server machine","status":"passed","title":"the linked-file tools link, capture, and relink one file on the server machine","duration":170.21937799999978,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572704064,"endTime":1791572705836.2195,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/mcp-modern-http.test.ts"},{"assertionResults":[{"ancestorTitles":["queued-work nudges on MCP tool results"],"fullName":"queued-work nudges on MCP tool results NDG-001-B: a principal with queued mailbox dispatches sees exactly one queued-work nudge on an unrelated tool result, the nudge names the real count and steps down to the unfinished claim and the settling echo as the queue drains, and it disappears once nothing is pending","status":"passed","title":"NDG-001-B: a principal with queued mailbox dispatches sees exactly one queued-work nudge on an unrelated tool result, the nudge names the real count and steps down to the unfinished claim and the settling echo as the queue drains, and it disappears once nothing is pending","duration":645.467585,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["queued-work nudges on MCP tool results"],"fullName":"queued-work nudges on MCP tool results NDG-001-F: with work queued, an error result and a dispatch_inbox result each carry no nudge","status":"passed","title":"NDG-001-F: with work queued, an error result and a dispatch_inbox result each carry no nudge","duration":155.457312,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572963531,"endTime":1791572964332.4573,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/ndg-001-queued-work-nudge.test.ts"},{"assertionResults":[{"ancestorTitles":["nudge isolation on MCP tool results"],"fullName":"nudge isolation on MCP tool results NDG-002-B: structuredContent and the existing content items are byte-identical with and without a pending nudge, and the nudge is one bounded trailing item that quotes no untrusted text","status":"passed","title":"NDG-002-B: structuredContent and the existing content items are byte-identical with and without a pending nudge, and the nudge is one bounded trailing item that quotes no untrusted text","duration":279.67499399999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["nudge isolation on MCP tool results"],"fullName":"nudge isolation on MCP tool results NDG-002-F: a second principal calling the same tools sees none of the first principal's counts, with or without a mailbox agent of its own","status":"passed","title":"NDG-002-F: a second principal calling the same tools sees none of the first principal's counts, with or without a mailbox agent of its own","duration":187.555976,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573026332,"endTime":1791573026799.556,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/ndg-002-nudge-isolation.test.ts"},{"assertionResults":[{"ancestorTitles":["OIDC admission provider and credential boundaries"],"fullName":"OIDC admission provider and credential boundaries AUTH-029: an enabled browser policy cannot admit an unknown bearer identity","status":"passed","title":"AUTH-029: an enabled browser policy cannot admit an unknown bearer identity","duration":41.287645999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC admission provider and credential boundaries"],"fullName":"OIDC admission provider and credential boundaries AUTH-029: an enabled OIDC policy cannot admit an unknown WorkOS browser identity","status":"passed","title":"AUTH-029: an enabled OIDC policy cannot admit an unknown WorkOS browser identity","duration":13.757767999999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573046776,"endTime":1791573046831.7578,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/oidc-admission-boundary.test.ts"},{"assertionResults":[{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: admission defaults off and an enabled policy normalizes its issuer and domains","status":"passed","title":"AUTH-029: admission defaults off and an enabled policy normalizes its issuer and domains","duration":5.00476500000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects nonliteral auto-admission switch \"TRUE\"","status":"passed","title":"AUTH-029: rejects nonliteral auto-admission switch \"TRUE\"","duration":2.076486999999929,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects nonliteral auto-admission switch \"False\"","status":"passed","title":"AUTH-029: rejects nonliteral auto-admission switch \"False\"","duration":0.4902190000000246,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects nonliteral auto-admission switch \"1\"","status":"passed","title":"AUTH-029: rejects nonliteral auto-admission switch \"1\"","duration":0.48700799999994615,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects nonliteral auto-admission switch \"yes\"","status":"passed","title":"AUTH-029: rejects nonliteral auto-admission switch \"yes\"","duration":0.4410609999999906,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects nonliteral auto-admission switch \" true\"","status":"passed","title":"AUTH-029: rejects nonliteral auto-admission switch \" true\"","duration":0.5063430000000153,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects nonliteral auto-admission switch \"\"","status":"passed","title":"AUTH-029: rejects nonliteral auto-admission switch \"\"","duration":0.41436399999997775,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"*.example.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"*.example.test\" even when disabled","duration":0.45748199999991357,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example.test,\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example.test,\" even when disabled","duration":0.377837999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \",example.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \",example.test\" even when disabled","duration":0.5353250000000571,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example..test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example..test\" even when disabled","duration":0.30346700000006877,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"-example.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"-example.test\" even when disabled","duration":0.2076790000000983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example-.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example-.test\" even when disabled","duration":0.18621700000005603,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"https://example.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"https://example.test\" even when disabled","duration":0.20520999999996548,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example.test:443\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example.test:443\" even when disabled","duration":0.18421200000000226,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"user@example.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"user@example.test\" even when disabled","duration":0.21319900000003145,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example_test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example_test\" even when disabled","duration":0.19701899999995476,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example.test.\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example.test.\" even when disabled","duration":0.1661829999999327,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"example test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"example test\" even when disabled","duration":0.16003200000000106,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: rejects malformed exact-domain list \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test\" even when disabled","status":"passed","title":"AUTH-029: rejects malformed exact-domain list \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test\" even when disabled","duration":0.15372899999999845,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: policy settings require complete OIDC configuration {\"ARTIFACT_SERVER_OIDC_AUTO_ADMIT\":\"false\"}","status":"passed","title":"AUTH-029: policy settings require complete OIDC configuration {\"ARTIFACT_SERVER_OIDC_AUTO_ADMIT\":\"false\"}","duration":0.4027969999999641,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: policy settings require complete OIDC configuration {\"ARTIFACT_SERVER_OIDC_AUTO_ADMIT\":\"true\"}","status":"passed","title":"AUTH-029: policy settings require complete OIDC configuration {\"ARTIFACT_SERVER_OIDC_AUTO_ADMIT\":\"true\"}","duration":0.32106499999997595,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: policy settings require complete OIDC configuration {\"ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS\":\"\"}","status":"passed","title":"AUTH-029: policy settings require complete OIDC configuration {\"ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS\":\"\"}","duration":0.21632599999998092,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic-admission configuration"],"fullName":"OIDC automatic-admission configuration AUTH-029: the loaded policy admits a new member through real browser login","status":"passed","title":"AUTH-029: the loaded policy admits a new member through real browser login","duration":133.02607299999988,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573043397,"endTime":1791573043544.0261,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/oidc-admission-configuration.test.ts"},{"assertionResults":[{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029-F: an unadmitted OIDC user remains denied by default","status":"passed","title":"AUTH-029-F: an unadmitted OIDC user remains denied by default","duration":139.12069499999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029-B: enabled browser admission creates a normal member and reuses its identity","status":"passed","title":"AUTH-029-B: enabled browser admission creates a normal member and reuses its identity","duration":60.68001200000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: automatic admission preserves first-administrator bootstrap","status":"passed","title":"AUTH-029: automatic admission preserves first-administrator bootstrap","duration":33.83362899999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: inactive identity stays denied with 'member@ackstorm.com' and 'different-subject'","status":"passed","title":"AUTH-029: inactive identity stays denied with 'member@ackstorm.com' and 'different-subject'","duration":52.7784640000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: inactive identity stays denied with 'changed@ackstorm.com' and 'member-subject'","status":"passed","title":"AUTH-029: inactive identity stays denied with 'changed@ackstorm.com' and 'member-subject'","duration":44.881444999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: inactive identity stays denied with 'other@ackstorm.com' and 'member-subject'","status":"passed","title":"AUTH-029: inactive identity stays denied with 'other@ackstorm.com' and 'member-subject'","duration":95.73402399999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: simultaneous first logins converge on one member","status":"passed","title":"AUTH-029: simultaneous first logins converge on one member","duration":41.84170999999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: enabled admission still refuses an explicitly unverified email","status":"passed","title":"AUTH-029: enabled admission still refuses an explicitly unverified email","duration":28.277964999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: a policy for another issuer cannot admit a verified user","status":"passed","title":"AUTH-029: a policy for another issuer cannot admit a verified user","duration":31.427679000000126,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: a token with a foreign signature creates no member or session","status":"passed","title":"AUTH-029: a token with a foreign signature creates no member or session","duration":31.83343099999979,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: explicitly admitted administrators keep their role","status":"passed","title":"AUTH-029: explicitly admitted administrators keep their role","duration":33.000508999999965,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: a listed domain admits a member after email normalization","status":"passed","title":"AUTH-029: a listed domain admits a member after email normalization","duration":30.29578700000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: domain allowlisting refuses member@gmail.com","status":"passed","title":"AUTH-029: domain allowlisting refuses member@gmail.com","duration":33.81673100000012,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: domain allowlisting refuses member@sub.ackstorm.com","status":"passed","title":"AUTH-029: domain allowlisting refuses member@sub.ackstorm.com","duration":37.22592900000018,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: domain allowlisting refuses member@ackstorm.com.attacker.test","status":"passed","title":"AUTH-029: domain allowlisting refuses member@ackstorm.com.attacker.test","duration":33.4943330000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["OIDC automatic browser admission"],"fullName":"OIDC automatic browser admission AUTH-029: the domain allowlist preserves explicit admission outside its domains","status":"passed","title":"AUTH-029: the domain allowlist preserves explicit admission outside its domains","duration":34.970145,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572922599,"endTime":1791572923362.9702,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/oidc-auto-admission.test.ts"},{"assertionResults":[{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization discovery accepts one realm-path issuer and refuses a weaker contract","status":"passed","title":"discovery accepts one realm-path issuer and refuses a weaker contract","duration":146.34085299999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization MCP-013-B: an end-user token authenticates once and keeps its own membership","status":"passed","title":"MCP-013-B: an end-user token authenticates once and keeps its own membership","duration":296.80139799999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization a lean token resolves its profile through userinfo exactly once","status":"passed","title":"a lean token resolves its profile through userinfo exactly once","duration":123.462984,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization only this issuer, this algorithm, and these keys are accepted","status":"passed","title":"only this issuer, this algorithm, and these keys are accepted","duration":200.7322720000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization MCP-013-F: wrong token contracts fail closed and issuer outages stay distinct","status":"passed","title":"MCP-013-F: wrong token contracts fail closed and issuer outages stay distinct","duration":199.75830599999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization the access-token types issuers really send are accepted","status":"passed","title":"the access-token types issuers really send are accepted","duration":168.23024199999986,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization an unverified email cannot claim the administrator or link to a member","status":"passed","title":"an unverified email cannot claim the administrator or link to a member","duration":128.14195300000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization a token that names several audiences is accepted","status":"passed","title":"a token that names several audiences is accepted","duration":61.241111000000046,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["generic OIDC MCP authorization"],"fullName":"generic OIDC MCP authorization the MCP resource follows the application origin and nothing else opens it","status":"passed","title":"the MCP resource follows the application origin and nothing else opens it","duration":41.987380999999914,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572895143,"endTime":1791572896509.9873,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/oidc-mcp-authorization.test.ts"},{"assertionResults":[{"ancestorTitles":["private content sessions"],"fullName":"private content sessions AUTH-014-B AUTH-015-B: one bootstrap opens one private immutable site through a host-only session","status":"passed","title":"AUTH-014-B AUTH-015-B: one bootstrap opens one private immutable site through a host-only session","duration":222.44955600000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["private content sessions"],"fullName":"private content sessions AUTH-015-F: content sessions cannot cross version hosts or authorize writes","status":"passed","title":"AUTH-015-F: content sessions cannot cross version hosts or authorize writes","duration":107.77969999999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["private content sessions"],"fullName":"private content sessions CMT-018-B CMT-018-F: an authenticated exact-version preview lease is read-only, restart-safe, and version-bound","status":"passed","title":"CMT-018-B CMT-018-F: an authenticated exact-version preview lease is read-only, restart-safe, and version-bound","duration":145.3884129999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572965078,"endTime":1791572965554.3884,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/private-content-session.test.ts"},{"assertionResults":[{"ancestorTitles":["project-scoped artifacts"],"fullName":"project-scoped artifacts default use stays simple while project boundaries stay isolated","status":"passed","title":"default use stays simple while project boundaries stay isolated","duration":260.86992900000007,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["project-scoped artifacts"],"fullName":"project-scoped artifacts archive blocks writes but preserves exact history","status":"passed","title":"archive blocks writes but preserves exact history","duration":96.17836699999987,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572960374,"endTime":1791572960731.1785,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/project-scoped-artifacts.test.ts"},{"assertionResults":[{"ancestorTitles":["derived agent activity"],"fullName":"derived agent activity PRS-001-B: a fresh agent reads idle, a claimed bundle reads working with its dispatch id, and full resolution returns it to idle with the dispatch addressed","status":"passed","title":"PRS-001-B: a fresh agent reads idle, a claimed bundle reads working with its dispatch id, and full resolution returns it to idle with the dispatch addressed","duration":196.938215,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["derived agent activity"],"fullName":"derived agent activity PRS-001-F: a stale heartbeat reads disconnected while the agent still holds a claim, and repeated reads write no activity","status":"passed","title":"PRS-001-F: a stale heartbeat reads disconnected while the agent still holds a claim, and repeated reads write no activity","duration":58.60551299999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573041077,"endTime":1791573041332.6055,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/prs-001-derived-activity.test.ts"},{"assertionResults":[{"ancestorTitles":["agent activity beacon"],"fullName":"agent activity beacon PRS-002-B: a fresh beacon reports working with its finer state, decays to the derived state after the 60 s TTL, and an idle beacon clears it at once","status":"passed","title":"PRS-002-B: a fresh beacon reports working with its finer state, decays to the derived state after the 60 s TTL, and an idle beacon clears it at once","duration":154.85729600000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["agent activity beacon"],"fullName":"agent activity beacon PRS-002-F: a beacon from an agent whose heartbeat is stale is accepted with 204 but never surfaces in the listing","status":"passed","title":"PRS-002-F: a beacon from an agent whose heartbeat is stale is accepted with 204 but never surfaces in the listing","duration":34.37440199999992,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573050284,"endTime":1791573050473.3745,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/prs-002-activity-beacon.test.ts"},{"assertionResults":[{"ancestorTitles":["activity beacon authority"],"fullName":"activity beacon authority PRS-003-B: an agent principal beacons its own agent and the finer state surfaces","status":"passed","title":"PRS-003-B: an agent principal beacons its own agent and the finer state surfaces","duration":132.2663,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["activity beacon authority"],"fullName":"activity beacon authority PRS-003-F: another principal's beacon answers the same 404 as a missing agent and records nothing, and a direct human is refused outright","status":"passed","title":"PRS-003-F: another principal's beacon answers the same 404 as a missing agent and records nothing, and a direct human is refused outright","duration":81.77360699999997,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573048797,"endTime":1791573049011.7737,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/prs-003-beacon-authority.test.ts"},{"assertionResults":[{"ancestorTitles":["bulk comment clearing"],"fullName":"bulk comment clearing PRS-005-B: clearing resolved deletes exactly the resolved threads with one ledger action each, and a manage-any key clears all","status":"passed","title":"PRS-005-B: clearing resolved deletes exactly the resolved threads with one ledger action each, and a manage-any key clears all","duration":276.62726,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bulk comment clearing"],"fullName":"bulk comment clearing PRS-005-F: an actively dispatched thread is skipped and counted, a non-member key is refused, and MCP comment_clear answers the same shape","status":"passed","title":"PRS-005-F: an actively dispatched thread is skipped and counted, a non-member key is refused, and MCP comment_clear answers the same shape","duration":153.83578599999998,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573018251,"endTime":1791573018681.8357,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/prs-005-bulk-clear.test.ts"},{"assertionResults":[{"ancestorTitles":["registered agent kind widening"],"fullName":"registered agent kind widening PRS-007-B: any valid kind slug registers, round-trips through the listing, and the response carries protocol version 1","status":"passed","title":"PRS-007-B: any valid kind slug registers, round-trips through the listing, and the response carries protocol version 1","duration":106.57545299999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["registered agent kind widening"],"fullName":"registered agent kind widening PRS-007-F: a kind that is not a slug is refused with 422 and registers nothing","status":"passed","title":"PRS-007-F: a kind that is not a slug is refused with 422 and registers nothing","duration":76.43165499999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["registered agent kind widening"],"fullName":"registered agent kind widening PRS-007: declared capabilities normalize to known keys, unknown keys are ignored, and an absent declaration defaults to the native tier","status":"passed","title":"PRS-007: declared capabilities normalize to known keys, unknown keys are ignored, and an absent declaration defaults to the native tier","duration":65.03733199999999,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573042161,"endTime":1791573042409.0374,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/prs-007-agent-kind-slug.test.ts"},{"assertionResults":[{"ancestorTitles":["authenticated media preview delivery"],"fullName":"authenticated media preview delivery PRV-003-B: exact image and ranged video bytes use typed, immutable, media-only responses","status":"passed","title":"PRV-003-B: exact image and ranged video bytes use typed, immutable, media-only responses","duration":216.67783799999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["authenticated media preview delivery"],"fullName":"authenticated media preview delivery PRV-003-F: hostile contexts, unsupported types, and absent paths fail closed","status":"passed","title":"PRV-003-F: hostile contexts, unsupported types, and absent paths fail closed","duration":114.88769699999989,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573016169,"endTime":1791573016500.8877,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/prv-003-media-preview-route.test.ts"},{"assertionResults":[{"ancestorTitles":["public-link administration"],"fullName":"public-link administration AUTH-018-B: administrators page across projects and make one public artifact private","status":"passed","title":"AUTH-018-B: administrators page across projects and make one public artifact private","duration":304.46304699999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["public-link administration"],"fullName":"public-link administration AUTH-018-F: bulk shutdown rejects invalid requests and preserves partial-failure retry semantics","status":"passed","title":"AUTH-018-F: bulk shutdown rejects invalid requests and preserves partial-failure retry semantics","duration":127.55387099999996,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572924010,"endTime":1791572924442.554,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/public-link-administration.test.ts"},{"assertionResults":[{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime ART-001-B: versions keep identity while publication links keep their distinct roles","status":"passed","title":"ART-001-B: versions keep identity while publication links keep their distinct roles","duration":257.26258500000006,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime PUB-006-B PUB-006-F: retries return the original result and conflicting reuse fails","status":"passed","title":"PUB-006-B PUB-006-F: retries return the original result and conflicting reuse fails","duration":55.49252999999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime PUB-008-B PUB-008-F: concurrent stale publishes cannot overwrite the winner","status":"passed","title":"PUB-008-B PUB-008-F: concurrent stale publishes cannot overwrite the winner","duration":70.25923899999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing runtime"],"fullName":"local publishing runtime PUB-004-B PUB-007-B: an intentional identical publish creates a version but reuses immutable bytes","status":"passed","title":"PUB-004-B PUB-007-B: an intentional identical publish creates a version but reuses immutable bytes","duration":54.257012999999915,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572966421,"endTime":1791572966859.257,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/publishing-runtime.test.ts"},{"assertionResults":[{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries MAN-003-B MAN-003-F: portable paths round-trip through URLs and unsafe paths are rejected","status":"passed","title":"MAN-003-B MAN-003-F: portable paths round-trip through URLs and unsafe paths are rejected","duration":189.966498,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries API publication requires a trusted principal and creates nothing on rejection","status":"passed","title":"API publication requires a trusted principal and creates nothing on rejection","duration":88.12847199999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries AUTH-002-B: account-required is the default and an admitted principal can open it","status":"passed","title":"AUTH-002-B: account-required is the default and an admitted principal can open it","duration":47.88684000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries PUB-002-F SCP-007-F: public APIs reject raw contents, paths, and source URLs","status":"passed","title":"PUB-002-F SCP-007-F: public APIs reject raw contents, paths, and source URLs","duration":29.206268999999793,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries foundation: malformed and oversized file-upload plans fail without destabilizing the server","status":"passed","title":"foundation: malformed and oversized file-upload plans fail without destabilizing the server","duration":550.2585199999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries foundation: the local server binds only to IPv4 loopback","status":"passed","title":"foundation: the local server binds only to IPv4 loopback","duration":12.846129999999903,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries AUTH-002-F: account-required bytes do not open through stable or exact links","status":"passed","title":"AUTH-002-F: account-required bytes do not open through stable or exact links","duration":30.355383000000074,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local publishing security boundaries"],"fullName":"local publishing security boundaries foundation: public content is retained at the edge but authorization is revalidated","status":"passed","title":"foundation: public content is retained at the edge but authorization is revalidated","duration":56.16651300000012,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572970282,"endTime":1791572971287.1665,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/publishing-security.test.ts"},{"assertionResults":[{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle PUB-001-F: another principal or installation cannot use a staged upload","status":"passed","title":"PUB-001-F: another principal or installation cannot use a staged upload","duration":79.79069900000002,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle expiry closes only uncommitted staging while committed retries remain stable","status":"passed","title":"expiry closes only uncommitted staging while committed retries remain stable","duration":39.98229200000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle PUB-009-B OPS-006-F: cleanup is bounded, waits for settle, tolerates concurrent passes, and preserves committed work","status":"passed","title":"PUB-009-B OPS-006-F: cleanup is bounded, waits for settle, tolerates concurrent passes, and preserves committed work","duration":36.11708199999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["staged upload lifecycle"],"fullName":"staged upload lifecycle foundation: an interrupted local write cannot reappear after cleanup","status":"passed","title":"foundation: an interrupted local write cannot reappear after cleanup","duration":14.633795999999961,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572903225,"endTime":1791572903395.6338,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/staged-upload-lifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["local storage migration"],"fullName":"local storage migration foundation: an early local database upgrades without losing publication","status":"passed","title":"foundation: an early local database upgrades without losing publication","duration":138.97785999999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local storage migration"],"fullName":"local storage migration a populated pre-project database migrates once without changing identity or bytes","status":"passed","title":"a populated pre-project database migrates once without changing identity or bytes","duration":62.92621299999996,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["local storage migration"],"fullName":"local storage migration foundation: the previous management schema upgrades to durable tag and deletion idempotency","status":"passed","title":"foundation: the previous management schema upgrades to durable tag and deletion idempotency","duration":105.34669799999995,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572920590,"endTime":1791572920897.3467,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/storage-migration.test.ts"},{"assertionResults":[{"ancestorTitles":["hosted WorkOS MCP authorization"],"fullName":"hosted WorkOS MCP authorization hosted startup accepts only exact refreshable S256 authorization metadata","status":"passed","title":"hosted startup accepts only exact refreshable S256 authorization metadata","duration":66.64948299999992,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["hosted WorkOS MCP authorization"],"fullName":"hosted WorkOS MCP authorization foundation: discovery, JWT verification, first-use identity binding, and API-key fallback share one live HTTP boundary","status":"passed","title":"foundation: discovery, JWT verification, first-use identity binding, and API-key fallback share one live HTTP boundary","duration":287.0246809999999,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["hosted WorkOS MCP authorization"],"fullName":"hosted WorkOS MCP authorization foundation: wrong token contracts fail closed and provider outages stay distinct","status":"passed","title":"foundation: wrong token contracts fail closed and provider outages stay distinct","duration":161.16542500000014,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572973228,"endTime":1791572973743.1655,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/conformance/workos-mcp-authorization.test.ts"},{"assertionResults":[{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract normalizes the shared defaults and accepts each native provider shape","status":"passed","title":"normalizes the shared defaults and accepts each native provider shape","duration":13.13464799999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract accepts isolated private DNS names without requiring a public suffix","status":"passed","title":"accepts isolated private DNS names without requiring a public suffix","duration":1.4393460000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract accepts complete Cloudflare OIDC configuration","status":"passed","title":"accepts complete Cloudflare OIDC configuration","duration":1.9342070000000149,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects floating image tag before a provider write","status":"passed","title":"rejects floating image tag before a provider write","duration":1.9310839999999985,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects same registrable content domain before a provider write","status":"passed","title":"rejects same registrable content domain before a provider write","duration":0.9331489999999576,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects same private DNS boundary before a provider write","status":"passed","title":"rejects same private DNS boundary before a provider write","duration":0.541134999999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects reversed capacity before a provider write","status":"passed","title":"rejects reversed capacity before a provider write","duration":0.4979380000000333,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects disabled production deletion protection before a provider write","status":"passed","title":"rejects disabled production deletion protection before a provider write","duration":0.4370739999999955,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects missing public DNS before a provider write","status":"passed","title":"rejects missing public DNS before a provider write","duration":0.4803380000000175,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects one zone for isolated domains before a provider write","status":"passed","title":"rejects one zone for isolated domains before a provider write","duration":0.3904190000000085,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects incomplete WorkOS configuration before a provider write","status":"passed","title":"rejects incomplete WorkOS configuration before a provider write","duration":0.40213899999997693,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects incomplete Cloudflare OIDC configuration before a provider write","status":"passed","title":"rejects incomplete Cloudflare OIDC configuration before a provider write","duration":0.445225999999991,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects Cloudflare OIDC configured beside WorkOS before a provider write","status":"passed","title":"rejects Cloudflare OIDC configured beside WorkOS before a provider write","duration":0.43252999999998565,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects Cloudflare OIDC fields on AWS before a provider write","status":"passed","title":"rejects Cloudflare OIDC fields on AWS before a provider write","duration":0.4602539999999635,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects credential-bearing telemetry URL before a provider write","status":"passed","title":"rejects credential-bearing telemetry URL before a provider write","duration":0.5350569999999948,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects credential-bearing state backend before a provider write","status":"passed","title":"rejects credential-bearing state backend before a provider write","duration":0.46825699999999415,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects main stack state-backend creation before a provider write","status":"passed","title":"rejects main stack state-backend creation before a provider write","duration":0.4827940000000126,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects unsupported direct Azure target before a provider write","status":"passed","title":"rejects unsupported direct Azure target before a provider write","duration":0.4342309999999543,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects local Alchemy state outside development before a provider write","status":"passed","title":"rejects local Alchemy state outside development before a provider write","duration":0.6209820000000263,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects invalid Cloudflare compatibility date before a provider write","status":"passed","title":"rejects invalid Cloudflare compatibility date before a provider write","duration":0.7334810000000402,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects non-global Cloudflare region before a provider write","status":"passed","title":"rejects non-global Cloudflare region before a provider write","duration":0.6224249999999643,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract accepts exact secret-free outputs and binds URLs to the requested stack","status":"passed","title":"accepts exact secret-free outputs and binds URLs to the requested stack","duration":2.1348710000000324,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects output drift, extra output keys, credential URLs, and known secrets","status":"passed","title":"rejects output drift, extra output keys, credential URLs, and known secrets","duration":1.923419000000024,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract keeps partial evidence for diagnosis but requires every passing gate for release","status":"passed","title":"keeps partial evidence for diagnosis but requires every passing gate for release","duration":3.6721640000000093,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects evidence for the wrong tool, impossible timestamps, and credential locations","status":"passed","title":"rejects evidence for the wrong tool, impossible timestamps, and credential locations","duration":1.4216800000000376,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["shared cloud deployment contract"],"fullName":"shared cloud deployment contract rejects duplicate release checks and unknown evidence fields","status":"passed","title":"rejects duplicate release checks and unknown evidence fields","duration":0.9200170000000298,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572916759,"endTime":1791572916796.92,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/deployment/cloud-deployment-contract.test.ts"},{"assertionResults":[{"ancestorTitles":["download filenames"],"fullName":"download filenames unsafe header characters cannot escape the attachment filename","status":"passed","title":"unsafe header characters cannot escape the attachment filename","duration":1.6243400000000037,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["download filenames"],"fullName":"download filenames an unusable filename gets a stable portable fallback","status":"passed","title":"an unusable filename gets a stable portable fallback","duration":0.18163299999999083,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573055691,"endTime":1791573055692.6243,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/http/content-disposition.test.ts"},{"assertionResults":[{"ancestorTitles":["linked artifacts over the local HTTP boundary"],"fullName":"linked artifacts over the local HTTP boundary a file links in place, drifts, and captures a new version","status":"passed","title":"a file links in place, drifts, and captures a new version","duration":167.03918799999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked artifacts over the local HTTP boundary"],"fullName":"linked artifacts over the local HTTP boundary every linked route answers the stable shape while the capability is off","status":"passed","title":"every linked route answers the stable shape while the capability is off","duration":31.30586500000004,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked artifacts over the local HTTP boundary"],"fullName":"linked artifacts over the local HTTP boundary the live origin streams current disk bytes to a session holder only","status":"passed","title":"the live origin streams current disk bytes to a session holder only","duration":48.38350300000002,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572989217,"endTime":1791572989463.3835,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/http/linked-artifacts.test.ts"},{"assertionResults":[{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: large JSON API responses are gzip-compressed when accepted","status":"passed","title":"foundation: large JSON API responses are gzip-compressed when accepted","duration":406.96679399999994,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: static assets compress, revalidate with 304, and keep immutable caching","status":"passed","title":"foundation: static assets compress, revalidate with 304, and keep immutable caching","duration":26.796771000000035,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: brotli is preferred over gzip when the client allows both","status":"passed","title":"foundation: brotli is preferred over gzip when the client allows both","duration":30.335937000000058,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: the application shell compresses without losing revalidation","status":"passed","title":"foundation: the application shell compresses without losing revalidation","duration":19.473770999999942,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: small and binary responses stay identity-encoded","status":"passed","title":"foundation: small and binary responses stay identity-encoded","duration":26.22250999999983,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node server response compression and asset caching"],"fullName":"Node server response compression and asset caching foundation: range-capable content responses are never compressed","status":"passed","title":"foundation: range-capable content responses are never compressed","duration":39.01789099999996,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572971948,"endTime":1791572972497.0178,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/http/node-response-compression.test.ts"},{"assertionResults":[{"ancestorTitles":["Node web asset store"],"fullName":"Node web asset store foundation: assets are read and digested once per process","status":"passed","title":"foundation: assets are read and digested once per process","duration":21.982855999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["Node web asset store"],"fullName":"Node web asset store foundation: HEAD serves headers without a body and misses are not cached","status":"passed","title":"foundation: HEAD serves headers without a body and misses are not cached","duration":1.9362020000000086,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573054457,"endTime":1791573054480.9363,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/http/node-web-asset-store.test.ts"},{"assertionResults":[{"ancestorTitles":["graceful HTTP shutdown"],"fullName":"graceful HTTP shutdown withdraws readiness and preserves accepted work before closing resources","status":"passed","title":"withdraws readiness and preserves accepted work before closing resources","duration":22.816270000000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["graceful HTTP shutdown"],"fullName":"graceful HTTP shutdown terminates stuck work at the configured deadline before closing resources","status":"passed","title":"terminates stuck work at the configured deadline before closing resources","duration":28.644761999999986,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573052175,"endTime":1791573052226.6448,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/lifecycle/graceful-http-shutdown.test.ts"},{"assertionResults":[{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization a relative path is rejected before any filesystem access","status":"passed","title":"a relative path is rejected before any filesystem access","duration":5.764655000000005,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization a missing target, a directory, and a dangling symlink are rejected","status":"passed","title":"a missing target, a directory, and a dangling symlink are rejected","duration":2.088175999999976,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization a symlink resolves to its target so containment is checked on the real file","status":"passed","title":"a symlink resolves to its target so containment is checked on the real file","duration":2.05540400000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","canonicalization"],"fullName":"linked source engine canonicalization error messages never include the presented path","status":"passed","title":"error messages never include the presented path","duration":1.0937529999999924,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","link roots"],"fullName":"linked source engine link roots dot-dot traversal cannot escape a root because paths canonicalize first","status":"passed","title":"dot-dot traversal cannot escape a root because paths canonicalize first","duration":1.1161789999999883,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","link roots"],"fullName":"linked source engine link roots a file inside any configured root passes","status":"passed","title":"a file inside any configured root passes","duration":1.5338639999999941,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","link roots"],"fullName":"linked source engine link roots a misconfigured root fails loudly instead of changing the boundary","status":"passed","title":"a misconfigured root fails loudly instead of changing the boundary","duration":4.300028999999995,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","self-protection"],"fullName":"linked source engine self-protection the data directory, its children, and the database companions are always rejected","status":"passed","title":"the data directory, its children, and the database companions are always rejected","duration":2.3549320000000193,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","self-protection"],"fullName":"linked source engine self-protection a database that lives outside the data directory is still protected","status":"passed","title":"a database that lives outside the data directory is still protected","duration":1.623960000000011,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","self-protection"],"fullName":"linked source engine self-protection an ordinary file outside the protected locations passes","status":"passed","title":"an ordinary file outside the protected locations passes","duration":1.162333999999987,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","freshness"],"fullName":"linked source engine freshness an untouched file is in-sync and a rewritten file is modified","status":"passed","title":"an untouched file is in-sync and a rewritten file is modified","duration":1.3959140000000048,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","freshness"],"fullName":"linked source engine freshness a deleted file is missing and a permission-denied file is unreadable","status":"passed","title":"a deleted file is missing and a permission-denied file is unreadable","duration":1.970455999999956,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","freshness"],"fullName":"linked source engine freshness a regular file replaced by a symlink is unreadable, never followed","status":"passed","title":"a regular file replaced by a symlink is unreadable, never followed","duration":0.7667380000000321,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open stat and bytes come from the same descriptor even when the path is swapped","status":"passed","title":"stat and bytes come from the same descriptor even when the path is swapped","duration":1.9603529999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open a symlink at the final path is refused by O_NOFOLLOW","status":"passed","title":"a symlink at the final path is refused by O_NOFOLLOW","duration":0.949935000000039,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open an expected-fingerprint mismatch aborts with the retryable drift error","status":"passed","title":"an expected-fingerprint mismatch aborts with the retryable drift error","duration":0.9512710000000197,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","verified open"],"fullName":"linked source engine verified open a missing file surfaces as missing","status":"passed","title":"a missing file surfaces as missing","duration":0.6890300000000025,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","capture"],"fullName":"linked source engine capture capture hashes and spools exactly the bytes of one descriptor pass","status":"passed","title":"capture hashes and spools exactly the bytes of one descriptor pass","duration":4.0810260000000085,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","capture"],"fullName":"linked source engine capture a modification during the read aborts the capture and removes the spool","status":"passed","title":"a modification during the read aborts the capture and removes the spool","duration":2.473443999999972,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["linked source engine","capture"],"fullName":"linked source engine capture a capture of a path swapped to a symlink is refused","status":"passed","title":"a capture of a path swapped to a symlink is refused","duration":1.0626889999999776,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572969514,"endTime":1791572969554.0627,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/local/linked-source-engine.test.ts"},{"assertionResults":[{"ancestorTitles":["native cloud object integrity helpers"],"fullName":"native cloud object integrity helpers an opened provider object must include the promised byte body","status":"passed","title":"an opened provider object must include the promised byte body","duration":1.817528999999979,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["native cloud object integrity helpers"],"fullName":"native cloud object integrity helpers a completed provider upload must report the exact accepted size","status":"passed","title":"a completed provider upload must report the exact accepted size","duration":0.29603899999995065,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573055562,"endTime":1791573055564.2961,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/storage/cloud-object-storage.test.ts"},{"assertionResults":[{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a link commit persists the first version and its source binding","status":"passed","title":"a link commit persists the first version and its source binding","duration":47.30172799999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a link commit replays under its original idempotency key","status":"passed","title":"a link commit replays under its original idempotency key","duration":12.189135000000022,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings recording freshness changes only the observed state","status":"passed","title":"recording freshness changes only the observed state","duration":12.718911999999989,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a captured version lands as an ordinary version row","status":"passed","title":"a captured version lands as an ordinary version row","duration":13.624092000000019,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a relink keeps the artifact identity and every saved version","status":"passed","title":"a relink keeps the artifact identity and every saved version","duration":14.857020999999975,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings a replayed relink appends no second action record","status":"passed","title":"a replayed relink appends no second action record","duration":11.561472999999978,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings binding operations reject an artifact without a binding","status":"passed","title":"binding operations reject an artifact without a binding","duration":13.895062999999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings an ordinary publication leaves every binding column null","status":"passed","title":"an ordinary publication leaves every binding column null","duration":8.72571899999997,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["sqlite source bindings"],"fullName":"sqlite source bindings the stored schema rejects a partial or unknown binding state","status":"passed","title":"the stored schema rejects a partial or unknown binding state","duration":12.763961999999992,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791572925778,"endTime":1791572925925.764,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/storage/sqlite-source-binding.test.ts"},{"assertionResults":[{"ancestorTitles":["syncDirectory platform behavior"],"fullName":"syncDirectory platform behavior syncs a real directory on POSIX platforms","status":"passed","title":"syncs a real directory on POSIX platforms","duration":2.823517999999993,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["syncDirectory platform behavior"],"fullName":"syncDirectory platform behavior skips the directory barrier on Windows instead of failing publication","status":"passed","title":"skips the directory barrier on Windows instead of failing publication","duration":0.5529989999999998,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["syncDirectory platform behavior"],"fullName":"syncDirectory platform behavior still reports a missing directory on POSIX platforms","status":"passed","title":"still reports a missing directory on POSIX platforms","duration":1.410820000000001,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["syncDirectory platform behavior"],"fullName":"syncDirectory platform behavior does not touch the filesystem on Windows","status":"passed","title":"does not touch the filesystem on Windows","duration":0.49220100000000855,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573055259,"endTime":1791573055264.4922,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/storage/verified-file-directory-sync.test.ts"},{"assertionResults":[{"ancestorTitles":["anti-slop Oxlint plugin"],"fullName":"anti-slop Oxlint plugin every configured rule rejects its representative unsafe pattern","status":"passed","title":"every configured rule rejects its representative unsafe pattern","duration":238.08592500000003,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["anti-slop Oxlint plugin"],"fullName":"anti-slop Oxlint plugin the plugin accepts precise types, direct calls, parsing, and justified assertions","status":"passed","title":"the plugin accepts precise types, direct calls, parsing, and justified assertions","duration":204.12618799999996,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573049143,"endTime":1791573049586.1262,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/tooling/anti-slop-plugin.test.ts"},{"assertionResults":[{"ancestorTitles":["object-storage provider boundary"],"fullName":"object-storage provider boundary DEP-022-F: provider SDK imports stay inside their adapters","status":"passed","title":"DEP-022-F: provider SDK imports stay inside their adapters","duration":19.014649000000006,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573054980,"endTime":1791573054999.0146,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/tooling/object-storage-boundary.test.ts"},{"assertionResults":[{"ancestorTitles":["bounded local runtime smoke"],"fullName":"bounded local runtime smoke publishes, reads concurrently, restarts, and stays within gross safety limits","status":"passed","title":"publishes, reads concurrently, restarts, and stays within gross safety limits","duration":1034.219231,"failureMessages":[],"meta":{},"tags":[]},{"ancestorTitles":["bounded local runtime smoke"],"fullName":"bounded local runtime smoke rejects a workload whose aggregate bytes would become a stress test","status":"passed","title":"rejects a workload whose aggregate bytes would become a stress test","duration":2.7155139999999847,"failureMessages":[],"meta":{},"tags":[]}],"startTime":1791573052935,"endTime":1791573053972.7156,"status":"passed","message":"","name":"/home/jcm/Projects/artifacts-server/tests/smoke/local-performance-smoke.test.ts"}]} \ No newline at end of file diff --git a/project/spec/conformance.yml b/project/spec/conformance.yml index 7e009b1f..7c979d8b 100644 --- a/project/spec/conformance.yml +++ b/project/spec/conformance.yml @@ -1,6 +1,6 @@ version: 1 product: Artifact Server -updated: "2026-08-26" +updated: "2026-10-09" evidence_root: "../.." source: @@ -1561,7 +1561,7 @@ requirements: source: {file: oidc-interactive-login-spec.md, anchor: conformance} acceptance: behavior: {id: AUTH-019-B, description: "Sign in against a real stub OIDC provider; an admitted member gets a session, the bootstrap administrator is admitted on first contact, display name and issuer-qualified binding are recorded, and a second login reuses the binding."} - failure: {id: AUTH-019-F, description: "An unadmitted verified identity, an unverified email, and a missing email are refused without creating members or sessions."} + failure: {id: AUTH-019-F, description: "With automatic admission disabled, an unadmitted verified identity, an unverified email, and a missing email are refused without creating members or sessions."} deployments: *all status: behavior_verified depends_on: [AUTH-001, AUTH-010] @@ -1710,6 +1710,25 @@ requirements: depends_on: [AUTH-001, AUTH-023, AUTH-025] evidence: [] + - id: AUTH-029 + kind: behavior + behavior: An installation may opt into normal-member admission only after validated browser OIDC login against its configured issuer, with an optional exact email-domain registration list, while preserving first-administrator bootstrap, existing roles, and deactivation. + owner: authorization + source: {file: oidc-auto-admission-spec.md, anchor: conformance} + acceptance: + behavior: {id: AUTH-029-B, description: "An eligible unknown browser user receives a normal member session, repeat and simultaneous logins reuse one member and binding, exact domains match after normalization, and bootstrap administrators and explicitly admitted users retain their roles and admission rules."} + failure: {id: AUTH-029-F, description: "Default-disabled admission, malformed policy configuration, disallowed email domains, another issuer, explicitly unverified email, invalid tokens, and inactive email or subject matches refuse access without creating a member or session; changed email cannot evade deactivation or installation/issuer scope."} + deployments: *all + status: behavior_verified + proof_gap: Local browser, configuration, repository, bearer, and WorkOS boundaries pass; complete packaged deployment verification and issuer-specific production login remain to be recorded. + depends_on: [AUTH-001, AUTH-019, AUTH-020, AUTH-021, AUTH-027] + evidence: + - deployment: local + tests: [AUTH-029-B, AUTH-029-F] + result: pass + run: project/evidence/local-foundation.json + recorded_at: "2026-10-09T19:11:51.885780+00:00" + - id: SEC-001 kind: security behavior: Published websites are treated as untrusted arbitrary browser JavaScript. diff --git a/project/spec/decisions/0020-generic-oidc-login.md b/project/spec/decisions/0020-generic-oidc-login.md index f77cd43d..49fa978b 100644 --- a/project/spec/decisions/0020-generic-oidc-login.md +++ b/project/spec/decisions/0020-generic-oidc-login.md @@ -153,3 +153,14 @@ repositories. This is the strictest reading of the claim, and it refuses logins from mainstream identity providers that never send it. The recorded decision above takes the industry-standard posture instead. + +## October 9, 2026: Optional browser admission + +[OIDC automatic browser admission](../oidc-auto-admission-spec.md) extends the +original explicit-admission decision with an opt-in policy for installations +whose configured issuer already controls membership eligibility. The default +remains explicit admission. The policy admits new browser users as normal +members, may restrict new registrations to exact email domains, and preserves +bootstrap administration, existing roles, and inactive-identity refusal. It does +not extend WorkOS, local-owner, or MCP bearer admission. Directory sync, SCIM, +role/group mapping, and administrator promotion remain excluded. diff --git a/project/spec/oidc-auto-admission-spec.md b/project/spec/oidc-auto-admission-spec.md new file mode 100644 index 00000000..91468cc0 --- /dev/null +++ b/project/spec/oidc-auto-admission-spec.md @@ -0,0 +1,93 @@ +# OIDC automatic browser admission + +**Status:** Accepted; local behavior verified — packaged deployment evidence remains to be recorded. +**Date:** October 9, 2026 +**Companion documents:** [OIDC browser login](./oidc-interactive-login-spec.md), +[Generic OIDC decision](./decisions/0020-generic-oidc-login.md), +[Conformance ledger](./conformance.yml) + +## Purpose + +An installation may delegate membership eligibility to its configured OIDC issuer. +For example, Dex may already restrict authentication to a corporate directory or +email domain. Such an installation can opt into admitting new users after a +validated browser login instead of requiring a separate manual admission for each +person. Explicit admission remains the default. + +## Configuration + +| Setting | Default | Meaning | +| --- | --- | --- | +| `ARTIFACT_SERVER_OIDC_AUTO_ADMIT` | `false` | Exactly `true` enables automatic admission; exactly `false` disables it. Other values fail startup. | +| `ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS` | Empty | Comma-separated exact email domains for new automatic registrations. An empty or whitespace-only value trusts the configured issuer's eligibility rules. | +| Helm `identity.oidcAutoAdmit` | `false` | Boolean equivalent of the environment switch. | +| Helm `identity.oidcAllowedEmailDomains` | `[]` | Array rendered as the comma-separated environment value. | + +Domains are trimmed and lowercased. Each entry must be a valid hostname, without +wildcards, empty entries, or a trailing dot. Matching uses the complete normalized +email domain: `example.com` accepts `person@EXAMPLE.COM`, but does not accept +`person@sub.example.com` or `person@otherexample.com`. + +The environment variables belong to the OIDC configuration family. Setting either +requires a complete OIDC issuer/client configuration and remains mutually exclusive +with WorkOS, even when the switch is `false`. Malformed domain values fail startup +even when automatic admission is disabled. These settings are not credentials. + +## Admission behavior + +The policy applies only after the configured browser OIDC flow has validated its +handshake cookie, single-use state, PKCE exchange, issuer, audience, signature, +expiry, nonce, and identity claims. It matches the issuer-qualified identity +provider, not the generic login-attempt discriminator. An explicit +`email_verified: false` remains denied; an absent claim keeps the existing +configured-issuer trust rule. + +Resolution looks for the same installation's issuer/subject binding before email. +With automatic admission enabled, both lookups include inactive members, so a +deactivated subject cannot evade the decision by changing its email, including to +another active member's email. An inactive email match is denied as well. Existing +active members keep their member ID and role; the registration-domain list does +not restrict explicitly admitted members. + +The first-member bootstrap rule remains unchanged: when the installation has no +members, only the configured bootstrap administrator may enter, and that member +receives the administrator role. The optional domain list does not restrict this +bootstrap. Once the installation has members, an unknown identity is admitted as +a normal member only when the policy is enabled for that issuer and the domain +list is empty or contains the complete email domain. Repeated or simultaneous +logins must converge on the same member and binding. Successful admission issues +the ordinary application session; denial creates no new member or session. + +The policy does not extend WorkOS admission, local-owner bootstrap, or MCP bearer +admission. A bearer identity cannot automatically register a normal member through +this setting. No schema migration, group mapping, administrator promotion, or +directory synchronization is introduced. + +## Conformance + +| ID | Acceptance | +| --- | --- | +| AUTH-029-B | Enable the policy and complete a real signed browser OIDC login for an unknown eligible person; receive a normal member session, reuse the member and binding on repeat/concurrent login, preserve first-administrator bootstrap and explicitly admitted roles, and accept an exact allowed domain after normalization. | +| AUTH-029-F | Keep automatic admission off by default; reject malformed policy configuration, an unknown person outside the exact domain list, another issuer, an explicitly unverified email, or an invalid token without creating a member/session; refuse inactive email/subject matches even after email changes, while retaining installation and issuer scope. | + +The acceptance tests use real services, HTTP OIDC boundaries, signed tokens, and +storage adapters: + +- `tests/conformance/oidc-auto-admission.test.ts` covers browser admission, + bootstrap, repeat/concurrent login, token rejection, domain boundaries, preserved + roles, and inactive identities with SQLite. +- `tests/conformance/oidc-admission-configuration.test.ts` covers parsing and the + loaded policy through a real browser login. +- `tests/conformance/auth-029-identity-repository.test.ts` covers status-inclusive + lookups and installation/issuer scope with SQLite. +- `tests/conformance/oidc-admission-boundary.test.ts` covers bearer-only and WorkOS + admission boundaries through real services and SQLite. +- `tests/integration/external-storage-runtime.test.ts` covers compiled and + in-process PostgreSQL replicas, simultaneous logins, and inactive-subject refusal. +- `deploy/cloudflare/tests/oidc-login-runtime.test.ts` covers Worker configuration + and a signed browser callback using D1. + +The ledger records passing local behavior evidence in `project/evidence/local-foundation.json`. +Helm rendering and the complete packaged deployment matrix also require their +normal verification gates; a passing local fixture alone does not prove production +login against an operator's issuer. diff --git a/project/spec/oidc-interactive-login-spec.md b/project/spec/oidc-interactive-login-spec.md index 9f946654..4908a0ab 100644 --- a/project/spec/oidc-interactive-login-spec.md +++ b/project/spec/oidc-interactive-login-spec.md @@ -11,7 +11,7 @@ Self-hosted Artifact Server browser login today supports exactly one identity pr The sibling Workspaces product already ships this: its self-host default is `AUTH_MODE=oidc` against any OpenID Connect provider, with WorkOS reserved for the hosted Cloudflare flavor (`SELF-HOST.md:83` refuses `AUTH_MODE=workos` in self-host). This spec brings Artifact Server to parity by adding a second, generic OIDC implementation of the existing provider port, deliberately mirroring Workspaces' configuration vocabulary, flow, and validation rules so the two products describe OIDC the same way. -Nothing else about identity changes. The port stays as it is; sessions, CSRF, admission, members, API keys, and the deployment token are untouched. OIDC only answers "who is this person"; explicit member admission still decides "may they enter." +Nothing else about identity changes in this original specification. The port stays as it is; sessions, CSRF, admission, members, API keys, and the deployment token are untouched. OIDC only answers "who is this person"; explicit member admission remains the default. [OIDC automatic browser admission](./oidc-auto-admission-spec.md) adds an optional issuer-scoped registration policy. ## 2. Goal and non-goals @@ -24,6 +24,10 @@ Nothing else about identity changes. The port stays as it is; sessions, CSRF, ad API keys, the deployment token, local browser login (`/auth/local`), and content sessions are unaffected. +The original provisioning exclusion above is extended only by the optional +[OIDC automatic browser-admission policy](./oidc-auto-admission-spec.md). +Directory sync, SCIM, and role/group mapping remain outside its scope. + ## 3. Configuration New variables, mirroring Workspaces' `OIDC_*` names under this repo's `ARTIFACT_SERVER_` prefix and secret conventions (`loadOptionalCredential` supports `` xor `_FILE`, `src/lifecycle/runtime-configuration.ts:554-595`): @@ -81,7 +85,7 @@ Flow, matching Workspaces' `providers/oidc.ts` decision for decision: - No id_token is accepted from any issuer other than the configured one; the JWKS is fetched only from the discovered `jwks_uri` under the issuer's validated origin rules; `alg` is whatever the JWKS key advertises via jose's verification (no `alg: none`, jose refuses it structurally). - HTTPS-only endpoints except explicit local-development hosts (section 3 rule). - Logged: provider name, issuer, and cause on discovery/exchange failure. Never logged: authorization codes, tokens, verifiers, client secret, id_token contents, email addresses at failure paths (match the existing generic `IdentityProviderFailure` message discipline). -- Admission unchanged and last: a verified OIDC identity that is not an admitted active member (or the bootstrap administrator on first contact, `installation-access.ts:417-440`) is refused with `IdentityAdmissionDenied`. +- Admission remains last: by default, a verified OIDC identity that is not an admitted active member (or the bootstrap administrator on first contact, `installation-access.ts:417-440`) is refused with `IdentityAdmissionDenied`. The optional [automatic-admission policy](./oidc-auto-admission-spec.md) can admit an eligible new browser user as a normal member after the same validation. ## 6. Deployment surface @@ -101,7 +105,7 @@ Module `authorization`, three new requirements in the existing ledger style (sou | ID | Kind | Behavior (short) | -B acceptance | -F acceptance | | --- | --- | --- | --- | --- | -| AUTH-019 | behavior | A deployment configured with a generic OIDC issuer completes browser login through discovery, auth-code with PKCE and nonce, id_token verification, and normal admission, on every deployment kind. | Sign in against a real stub OIDC provider; an admitted member gets a session, the bootstrap administrator is admitted on first contact, display name and issuer-qualified binding are recorded, and a second login reuses the binding. | An unadmitted verified identity, an unverified email, and a missing email are refused without creating members or sessions. | +| AUTH-019 | behavior | A deployment configured with a generic OIDC issuer completes browser login through discovery, auth-code with PKCE and nonce, id_token verification, and normal admission, on every deployment kind. | Sign in against a real stub OIDC provider; an admitted member gets a session, the bootstrap administrator is admitted on first contact, display name and issuer-qualified binding are recorded, and a second login reuses the binding. | With automatic admission disabled, an unadmitted verified identity, an unverified email, and a missing email are refused without creating members or sessions. | | AUTH-020 | security | The OIDC callback fails closed on every token-validation defect. | Valid logins succeed under a 30-second clock skew. | Wrong signature, wrong issuer, wrong audience, expired token, replayed or unknown state, expired attempt, and wrong nonce are each refused; no session or member is created; codes and tokens never appear in logs. | | AUTH-021 | constraint | OIDC configuration is all-or-nothing, mutually exclusive with WorkOS, and issuer/endpoint URLs obey the HTTPS rules. | A complete OIDC configuration boots and reports `interactiveIdentityProvider: "oidc"`; absence of both providers keeps login unavailable while keys still work. | Partial OIDC configuration, WorkOS-plus-OIDC together, a non-HTTPS non-local issuer, and discovery documents with a mismatched issuer or credentialed/fragmented endpoint URLs each fail startup or login without weakening the running server. | diff --git a/scripts/verify-helm-chart.sh b/scripts/verify-helm-chart.sh index 1063b7d4..fd48c857 100755 --- a/scripts/verify-helm-chart.sh +++ b/scripts/verify-helm-chart.sh @@ -21,6 +21,47 @@ artifactserver_values=( helm lint "$artifactserver_chart" --strict "${artifactserver_values[@]}" +assert_admission_environment() { + node --input-type=module -e ' + import assert from "node:assert/strict"; + import {readFileSync, realpathSync} from "node:fs"; + import {createRequire} from "node:module"; + const require = createRequire(realpathSync(`${process.argv[2]}/deploy/cloudflare/node_modules/wrangler/package.json`)); + const {parseAllDocuments} = require("yaml"); + const documents = parseAllDocuments(readFileSync(0, "utf8")).map((document) => document.toJS()); + const deployment = documents.find((document) => document.kind === "Deployment"); + const container = deployment.spec.template.spec.containers.find((entry) => entry.name === "artifact-server"); + const variables = new Map(container.env.map((entry) => [entry.name, entry.value])); + const names = ["ARTIFACT_SERVER_OIDC_AUTO_ADMIT", "ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS"]; + const mode = process.argv[1]; + const expected = mode === "enabled" ? ["true", "example.test,ackstorm.com"] : + mode === "disabled" ? ["false", ""] : [undefined, undefined]; + assert.deepEqual(names.map((name) => variables.get(name)), expected); + if (mode === "workos") assert.ok(names.every((name) => !variables.has(name))); + ' "$1" "$artifactserver_repository" +} + +helm template artifact-server "$artifactserver_chart" \ + --kube-version 1.36.0 \ + "${artifactserver_values[@]}" \ + --set identity.oidcAutoAdmit=true \ + --set-json 'identity.oidcAllowedEmailDomains=["example.test","ackstorm.com"]' | \ + assert_admission_environment enabled + +helm template artifact-server "$artifactserver_chart" \ + --kube-version 1.36.0 \ + "${artifactserver_values[@]}" | assert_admission_environment disabled + +helm template artifact-server "$artifactserver_chart" \ + --kube-version 1.36.0 \ + "${artifactserver_values[@]}" \ + --set identity.oidcClientId= \ + --set identity.oidcIssuer= \ + --set identity.workosClientId=client_workos \ + --set identity.workosIssuer=https://auth.example.test \ + --set secret.keys.workosApiKey=workos-api-key | \ + assert_admission_environment workos + for artifactserver_kubernetes_version in 1.34.0 1.35.0 1.36.0; do helm template artifact-server "$artifactserver_chart" \ --kube-version "$artifactserver_kubernetes_version" \ @@ -81,6 +122,30 @@ expect_render_failure "one installation has one browser-login provider" \ --set identity.workosIssuer=https://auth.example.test \ --set secret.keys.workosApiKey=workos-api-key +expect_render_failure "identity.oidcAutoAdmit and identity.oidcAllowedEmailDomains require" \ + "${artifactserver_values[@]}" \ + --set identity.oidcClientId= \ + --set identity.oidcIssuer= \ + --set identity.oidcAutoAdmit=true + +expect_render_failure "identity.oidcAutoAdmit and identity.oidcAllowedEmailDomains require" \ + "${artifactserver_values[@]}" \ + --set identity.oidcClientId= \ + --set identity.oidcIssuer= \ + --set-json 'identity.oidcAllowedEmailDomains=["example.test"]' + +expect_render_failure "oidcAllowedEmailDomains" \ + "${artifactserver_values[@]}" \ + --set-json 'identity.oidcAllowedEmailDomains=["*.example.test"]' + +expect_render_failure "oidcAutoAdmit" \ + "${artifactserver_values[@]}" \ + --set-string identity.oidcAutoAdmit=yes + +expect_render_failure "oidcAllowedEmailDomains" \ + "${artifactserver_values[@]}" \ + --set identity.oidcAllowedEmailDomains=example.test + expect_render_failure "private-team deployments require exactly one browser-login provider" \ "${artifactserver_values[@]}" \ --set identity.oidcClientId= \ diff --git a/src/application/installation-access.ts b/src/application/installation-access.ts index 2c9e4e8a..539feee5 100644 --- a/src/application/installation-access.ts +++ b/src/application/installation-access.ts @@ -22,6 +22,7 @@ import type { import { type ApplicationSession, type ExternalIdentity, + type ExternalIdentityAdmissionPolicy, type InstallationMember, type IssuedApplicationSession, type IssuedManagedApiKey, @@ -101,6 +102,15 @@ export interface InstallationIdentityRepository { installationId: string, email: string, ) => Effect.Effect; + readonly findMemberByEmail: ( + installationId: string, + email: string, + ) => Effect.Effect; + readonly findMemberByExternalIdentity: ( + installationId: string, + provider: string, + subject: string, + ) => Effect.Effect; readonly findActiveMemberByExternalIdentity: ( installationId: string, provider: string, @@ -171,6 +181,7 @@ export interface InstallationAccessDependencies { */ readonly authenticationCache?: AuthenticationCachePolicy | null; readonly bootstrapAdministratorEmail: string; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; readonly clock: {readonly now: () => Date}; readonly ids: IdentityIdProvider; readonly installationId: string; @@ -435,29 +446,44 @@ function makeInstallationAccessService( const completeExternalIdentity = Effect.fn( "InstallationAccessService.completeExternalIdentity", )(function*(identity: ExternalIdentity) { - const member = yield* resolveExternalMember(identity); + const member = yield* resolveExternalMember(identity, true); return yield* issueSession(member); }); const resolveExternalMember = Effect.fn( "InstallationAccessService.resolveExternalMember", - )(function*(identity: ExternalIdentity) { + )(function*(identity: ExternalIdentity, browserLogin = false) { if (!identity.emailVerified) { return yield* Effect.fail(new IdentityAdmissionDenied({ message: "The login provider did not verify the email address.", })); } const email = yield* normalizeEmail(identity.email); - let member = yield* dependencies.repository.findActiveMemberByExternalIdentity( - dependencies.installationId, - identity.provider, - identity.subject, - ); - if (member === null) { - member = yield* dependencies.repository.findActiveMemberByEmail( - dependencies.installationId, - email, + const policy = browserLogin && + dependencies.externalIdentityAdmissionPolicy?.provider === identity.provider + ? dependencies.externalIdentityAdmissionPolicy + : null; + let member = yield* policy === null + ? dependencies.repository.findActiveMemberByExternalIdentity( + dependencies.installationId, identity.provider, identity.subject, + ) + : dependencies.repository.findMemberByExternalIdentity( + dependencies.installationId, identity.provider, identity.subject, ); + if (member !== null && member.status !== memberStatuses.active) { + return yield* Effect.fail(new IdentityAdmissionDenied({ + message: "This person has not been admitted to the Artifact Server.", + })); + } + if (member === null) { + member = yield* policy === null + ? dependencies.repository.findActiveMemberByEmail(dependencies.installationId, email) + : dependencies.repository.findMemberByEmail(dependencies.installationId, email); + } + if (member !== null && member.status !== memberStatuses.active) { + return yield* Effect.fail(new IdentityAdmissionDenied({ + message: "This person has not been admitted to the Artifact Server.", + })); } if (member === null) { const hasMembers = yield* dependencies.repository.hasMembers( @@ -466,22 +492,38 @@ function makeInstallationAccessService( const bootstrapAdministratorEmail = yield* normalizeEmail( dependencies.bootstrapAdministratorEmail, ); - if ( - hasMembers || - email !== bootstrapAdministratorEmail - ) { + const bootstrap = !hasMembers && email === bootstrapAdministratorEmail; + if (!bootstrap && ( + policy === null || !hasMembers || + (policy.allowedEmailDomains.length > 0 && + !policy.allowedEmailDomains.includes(email.slice(email.lastIndexOf("@") + 1))) + )) { return yield* Effect.fail(new IdentityAdmissionDenied({ message: "This person has not been admitted to the Artifact Server.", })); } - member = yield* dependencies.repository.admitMember({ + const admission = dependencies.repository.admitMember({ createdAt: dependencies.clock.now().toISOString(), displayName: identity.displayName, email, id: dependencies.ids.memberId(), installationId: dependencies.installationId, - role: membershipRoles.administrator, + role: bootstrap ? membershipRoles.administrator : membershipRoles.member, }); + member = yield* bootstrap || policy === null ? admission : admission.pipe( + Effect.catchTag("IdentityConflict", (conflict) => Effect.gen(function*() { + const admitted = yield* dependencies.repository.findMemberByEmail( + dependencies.installationId, email, + ); + if (admitted === null) return yield* Effect.fail(conflict); + if (admitted.status !== memberStatuses.active) { + return yield* Effect.fail(new IdentityAdmissionDenied({ + message: "This person has not been admitted to the Artifact Server.", + })); + } + return admitted; + })), + ); } yield* dependencies.repository.bindExternalIdentity({ boundAt: dependencies.clock.now().toISOString(), diff --git a/src/cli/lifecycle-commands.ts b/src/cli/lifecycle-commands.ts index 8f6e8cd9..6e7021dd 100644 --- a/src/cli/lifecycle-commands.ts +++ b/src/cli/lifecycle-commands.ts @@ -427,22 +427,29 @@ async function startCompactServer( return startLocalServer(serverConfig); } -type OidcServerAuthentication = +type OidcServerAuthentication = ( | OidcHostedAuthentication - | Pick; + | Pick +) & Pick; /** MCP OAuth needs the issuer at startup; browser login must survive it being down. */ async function oidcAuthenticationOrBrowserOnly( oidc: OidcConfiguration, ): Promise { + const admission = oidc.externalIdentityAdmissionPolicy === undefined + ? {} + : {externalIdentityAdmissionPolicy: oidc.externalIdentityAdmissionPolicy}; try { - return await createOidcHostedAuthentication(oidc); + return {...await createOidcHostedAuthentication(oidc), ...admission}; } catch (cause) { const reason = cause instanceof Error ? cause.message : "the request failed"; process.stderr.write( `OIDC configuration warning (discovery_failed): MCP OAuth stays off for ${oidc.issuer}: ${reason}\n`, ); - return {interactiveIdentityProvider: createOidcIdentityProvider(oidc)}; + return { + interactiveIdentityProvider: createOidcIdentityProvider(oidc), + ...admission, + }; } } diff --git a/src/cli/oidc-configuration.ts b/src/cli/oidc-configuration.ts index 8fd42d8a..98cad8aa 100644 --- a/src/cli/oidc-configuration.ts +++ b/src/cli/oidc-configuration.ts @@ -1,6 +1,9 @@ import {type Redacted, Schema} from "effect"; import {z} from "zod"; +import type {ExternalIdentityAdmissionPolicy} from + "../core/installation-identity.js"; +import {parseOidcAdmissionPolicy} from "../identity/oidc-admission-policy.js"; import {defaultOidcScopes} from "../identity/oidc-identity-provider.js"; import {requireOidcIssuer} from "../identity/oidc-issuer.js"; import { @@ -11,6 +14,8 @@ import {runCliEffect} from "./run-cli-effect.js"; const oidcEnvironmentSchema = z.object({ ARTIFACT_SERVER_BOOTSTRAP_ADMIN_EMAIL: z.email().optional(), + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: z.string().optional(), + ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: z.string().optional(), ARTIFACT_SERVER_OIDC_CLIENT_ID: z.string().min(1).optional(), ARTIFACT_SERVER_OIDC_ISSUER: z.string().min(1).optional(), ARTIFACT_SERVER_OIDC_SCOPES: z.string().min(1).optional(), @@ -23,6 +28,7 @@ export interface OidcConfiguration { readonly bootstrapAdministratorEmail: string; readonly clientId: string; readonly clientSecret: Redacted.Redacted | null; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; readonly issuer: string; readonly scopes: string; } @@ -41,7 +47,9 @@ export async function loadOidcConfiguration( clientSecret === null && parsed.ARTIFACT_SERVER_OIDC_CLIENT_ID === undefined && parsed.ARTIFACT_SERVER_OIDC_ISSUER === undefined && - parsed.ARTIFACT_SERVER_OIDC_SCOPES === undefined + parsed.ARTIFACT_SERVER_OIDC_SCOPES === undefined && + parsed.ARTIFACT_SERVER_OIDC_AUTO_ADMIT === undefined && + parsed.ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS === undefined ) { return null; } @@ -56,19 +64,29 @@ export async function loadOidcConfiguration( "Generic OIDC authentication requires ARTIFACT_SERVER_ORIGIN, ARTIFACT_SERVER_BOOTSTRAP_ADMIN_EMAIL, ARTIFACT_SERVER_OIDC_ISSUER, and ARTIFACT_SERVER_OIDC_CLIENT_ID. ARTIFACT_SERVER_OIDC_CLIENT_SECRET or ARTIFACT_SERVER_OIDC_CLIENT_SECRET_FILE and ARTIFACT_SERVER_OIDC_SCOPES are optional.", ); } - return { + const issuer = requireOidcIssuer( + requireString(parsed.ARTIFACT_SERVER_OIDC_ISSUER), + "ARTIFACT_SERVER_OIDC_ISSUER", + ); + const policy = parseOidcAdmissionPolicy( + issuer, + parsed.ARTIFACT_SERVER_OIDC_AUTO_ADMIT, + parsed.ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS, + ); + const configuration: OidcConfiguration = { applicationOrigin: requireString(parsed.ARTIFACT_SERVER_ORIGIN), bootstrapAdministratorEmail: requireString( parsed.ARTIFACT_SERVER_BOOTSTRAP_ADMIN_EMAIL, ), clientId: requireString(parsed.ARTIFACT_SERVER_OIDC_CLIENT_ID), clientSecret: credentialOrNull(clientSecret), - issuer: requireOidcIssuer( - requireString(parsed.ARTIFACT_SERVER_OIDC_ISSUER), - "ARTIFACT_SERVER_OIDC_ISSUER", - ), + issuer, scopes: parsed.ARTIFACT_SERVER_OIDC_SCOPES ?? defaultOidcScopes, }; + return policy === null ? configuration : { + ...configuration, + externalIdentityAdmissionPolicy: policy, + }; } /** @@ -97,7 +115,11 @@ function hasBrowserLoginVariable( prefix: string, ): boolean { return Object.entries(environment).some(([name, value]) => - name.startsWith(prefix) && value !== undefined && value.trim() !== "" + name.startsWith(prefix) && value !== undefined && ( + value.trim() !== "" || + name === "ARTIFACT_SERVER_OIDC_AUTO_ADMIT" || + name === "ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS" + ) ); } diff --git a/src/core/errors.ts b/src/core/errors.ts index 389f4c7f..da83a4d2 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -439,6 +439,8 @@ export class IdentityRepositoryFailure extends Schema.TaggedError; + /** Find an admitted email, including inactive members. */ + findMemberByEmail( + installationId: string, + email: string, + ): Promise; + /** Find a bound subject, including inactive members. */ + findMemberByExternalIdentity( + installationId: string, + provider: string, + subject: string, + ): Promise; hasMembers(installationId: string): Promise; listApiKeys(installationId: string): Promise; listMembers(installationId: string): Promise; diff --git a/src/core/installation-identity.ts b/src/core/installation-identity.ts index e3572305..94374af3 100644 --- a/src/core/installation-identity.ts +++ b/src/core/installation-identity.ts @@ -39,6 +39,12 @@ export interface ExternalIdentity { readonly subject: string; } +/** Optional automatic admission for one interactive identity provider. */ +export interface ExternalIdentityAdmissionPolicy { + readonly allowedEmailDomains: readonly string[]; + readonly provider: string; +} + /** One server-side browser session after its opaque credential is verified. */ export interface ApplicationSession { readonly createdAt: string; diff --git a/src/external-storage/create-external-storage-runtime.ts b/src/external-storage/create-external-storage-runtime.ts index 9b993342..e26c2ce0 100644 --- a/src/external-storage/create-external-storage-runtime.ts +++ b/src/external-storage/create-external-storage-runtime.ts @@ -8,6 +8,7 @@ import type { import type {InteractiveIdentityProvider} from "../application/interactive-login.js"; import type {Clock} from "../core/ports.js"; import type {BrowserAccess} from "../core/browser-access.js"; +import type {ExternalIdentityAdmissionPolicy} from "../core/installation-identity.js"; import {SystemClock, SystemIdGenerator} from "../core/system.js"; import {createHttpApp} from "../http/create-http-app.js"; import type { @@ -70,6 +71,7 @@ export interface ExternalStorageRuntimeConfig { readonly apiOAuthResource?: ApiOAuthResourceConfiguration; readonly applicationOrigin?: string; readonly bootstrapAdministratorEmail: string; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; readonly browserAccess: Extract; readonly clock?: Clock; readonly completedRequestLogSampleRate?: number; @@ -178,6 +180,9 @@ export async function createExternalStorageRuntime( staging, stagingCleanupPolicy, }; + if (config.externalIdentityAdmissionPolicy !== undefined) { + Object.assign(applicationAdapters, {externalIdentityAdmissionPolicy: config.externalIdentityAdmissionPolicy}); + } if (gitHistoryProvider !== null) { Object.assign(applicationAdapters, {gitHistoryProvider}); } diff --git a/src/identity/oidc-admission-policy.ts b/src/identity/oidc-admission-policy.ts new file mode 100644 index 00000000..5446f92c --- /dev/null +++ b/src/identity/oidc-admission-policy.ts @@ -0,0 +1,36 @@ +import {z} from "zod"; + +import type {ExternalIdentityAdmissionPolicy} from + "../core/installation-identity.js"; +import {requireOidcIssuer} from "./oidc-issuer.js"; + +const autoAdmitSchema = z.enum(["true", "false"]); +const emailDomainSchema = z.string().max(253).regex( + /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/u, +); + +/** Validate operator settings for browser-only OIDC member admission. */ +export function parseOidcAdmissionPolicy( + issuer: string, + autoAdmit: string | undefined, + allowedEmailDomains: string | undefined, +): ExternalIdentityAdmissionPolicy | null { + const enabled = autoAdmitSchema.safeParse(autoAdmit ?? "false"); + if (!enabled.success) { + throw new Error("ARTIFACT_SERVER_OIDC_AUTO_ADMIT must be exactly true or false."); + } + const domains = allowedEmailDomains === undefined || + allowedEmailDomains.trim() === "" + ? [] + : allowedEmailDomains.split(",").map((domain) => domain.trim().toLowerCase()); + if (domains.some((domain) => !emailDomainSchema.safeParse(domain).success)) { + throw new Error( + "ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS must contain comma-separated exact hostnames without wildcards.", + ); + } + if (enabled.data === "false") return null; + return { + allowedEmailDomains: domains, + provider: `oidc:${requireOidcIssuer(issuer, "ARTIFACT_SERVER_OIDC_ISSUER")}`, + }; +} diff --git a/src/local/create-local-application-layer.ts b/src/local/create-local-application-layer.ts index 3e390758..d72655dc 100644 --- a/src/local/create-local-application-layer.ts +++ b/src/local/create-local-application-layer.ts @@ -10,6 +10,7 @@ import { import { digestIdentitySecret, InstallationAccessService, + type InstallationAccessDependencies, } from "../application/installation-access.js"; import { type InteractiveIdentityProvider, @@ -125,6 +126,7 @@ import type { StagingStore, } from "../core/ports.js"; import type {IdentityRepository} from "../core/identity-ports.js"; +import type {ExternalIdentityAdmissionPolicy} from "../core/installation-identity.js"; import type { ManifestEntry } from "../core/model.js"; import {randomBase64Url} from "../core/random.js"; import { FileVerificationError } from "../storage/verified-file.js"; @@ -145,6 +147,7 @@ export interface ApplicationAdapters { readonly apiToken: Redacted.Redacted | null; readonly blobs: BlobStore; readonly bootstrapAdministratorEmail: string; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; readonly clock: Clock; readonly externalApiBearerVerifier: BearerCredentialVerifier | null; readonly externalMcpBearerVerifier: BearerCredentialVerifier | null; @@ -757,7 +760,7 @@ export function createApplicationLayer( membershipRole: membershipRoles.member, }; const identityRepository = adapters.identityRepository; - const identityLayer = InstallationAccessService.layer({ + const identityDependencies: InstallationAccessDependencies = { bootstrapAdministratorEmail: adapters.bootstrapAdministratorEmail, clock: adapters.clock, ids: { @@ -812,6 +815,14 @@ export function createApplicationLayer( "findActiveMemberByEmail", () => identityRepository.findActiveMemberByEmail(installationId, email), ), + findMemberByEmail: (installationId, email) => identityEffect( + "findMemberByEmail", + () => identityRepository.findMemberByEmail(installationId, email), + ), + findMemberByExternalIdentity: (installationId, provider, subject) => identityEffect( + "findMemberByExternalIdentity", + () => identityRepository.findMemberByExternalIdentity(installationId, provider, subject), + ), findActiveMemberByExternalIdentity: ( installationId, provider, @@ -882,7 +893,11 @@ export function createApplicationLayer( issue: () => randomBase64Url(32), }, sessionLifetimeMilliseconds: 12 * 60 * 60 * 1_000, - }); + }; + if (adapters.externalIdentityAdmissionPolicy !== undefined) { + Object.assign(identityDependencies, {externalIdentityAdmissionPolicy: adapters.externalIdentityAdmissionPolicy}); + } + const identityLayer = InstallationAccessService.layer(identityDependencies); const authenticationLayer = Layer.effect( AuthenticationService, InstallationAccessService.use((installationAccess) => { diff --git a/src/local/create-local-runtime.ts b/src/local/create-local-runtime.ts index 42fe1188..01c2ace4 100644 --- a/src/local/create-local-runtime.ts +++ b/src/local/create-local-runtime.ts @@ -10,6 +10,7 @@ import type { import type { InteractiveIdentityProvider } from "../application/interactive-login.js"; import type { Clock } from "../core/ports.js"; import type {BrowserAccess} from "../core/browser-access.js"; +import type {ExternalIdentityAdmissionPolicy} from "../core/installation-identity.js"; import { SystemClock, SystemIdGenerator } from "../core/system.js"; import { createHttpApp } from "../http/create-http-app.js"; import type { @@ -83,6 +84,7 @@ export interface LocalRuntimeConfig { readonly apiOAuthResource?: ApiOAuthResourceConfiguration; readonly applicationOrigin?: string; readonly bootstrapAdministratorEmail?: string; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; readonly browserAccess: BrowserAccess; readonly clock?: Clock; readonly completedRequestLogSampleRate?: number; @@ -239,6 +241,9 @@ export async function createLocalRuntime( staging, stagingCleanupPolicy, }; + if (config.externalIdentityAdmissionPolicy !== undefined) { + Object.assign(applicationAdapters, {externalIdentityAdmissionPolicy: config.externalIdentityAdmissionPolicy}); + } if (gitHistoryProvider !== null) { Object.assign(applicationAdapters, {gitHistoryProvider}); } diff --git a/src/storage/postgres-identity-repository.ts b/src/storage/postgres-identity-repository.ts index 281e2d27..578311c1 100644 --- a/src/storage/postgres-identity-repository.ts +++ b/src/storage/postgres-identity-repository.ts @@ -212,6 +212,45 @@ export class PostgresIdentityRepository implements BootstrapManagedApiKeyReposit })); } + async findMemberByEmail( + installationId: string, + email: string, + ): Promise { + this.#assertInstallationScope(installationId); + return this.#database.run(Effect.gen({self: this}, function*() { + const sql = yield* SqlClient; + const rows = yield* sql.unsafe(memberSelect( + "WHERE installation_id = $1 AND email = $2", + ), [installationId, email]); + return memberRowSchema.nullable().parse(rows[0] ?? null); + })); + } + + async findMemberByExternalIdentity( + installationId: string, + provider: string, + subject: string, + ): Promise { + this.#assertInstallationScope(installationId); + return this.#database.run(Effect.gen({self: this}, function*() { + const sql = yield* SqlClient; + const rows = yield* sql.unsafe( + `SELECT member.id, member.installation_id AS "installationId", + member.email, member.display_name AS "displayName", member.role, + member.status, member.created_at AS "createdAt", + member.updated_at AS "updatedAt" + FROM external_identities AS external + JOIN installation_members AS member + ON member.installation_id = external.installation_id + AND member.id = external.member_id + WHERE member.installation_id = $1 AND external.provider = $2 + AND external.subject = $3`, + [installationId, provider, subject], + ); + return memberRowSchema.nullable().parse(rows[0] ?? null); + })); + } + async bindExternalIdentity(command: BindExternalIdentityRecord): Promise { const installationId = this.#installationId; const existingMember = await this.findMember( diff --git a/src/storage/sqlite-identity-repository.ts b/src/storage/sqlite-identity-repository.ts index db8ca556..b15d6c11 100644 --- a/src/storage/sqlite-identity-repository.ts +++ b/src/storage/sqlite-identity-repository.ts @@ -251,6 +251,50 @@ export class SqliteIdentityRepository implements BootstrapManagedApiKeyRepositor return row === undefined ? null : memberRowSchema.parse(row); } + async findMemberByEmail( + installationId: string, + email: string, + ): Promise { + const row = this.#database.prepare(` + SELECT + id, + installation_id AS installationId, + email, + display_name AS displayName, + role, + status, + created_at AS createdAt, + updated_at AS updatedAt + FROM installation_members + WHERE installation_id = ? AND email = ? + `).get(installationId, email); + return row === undefined ? null : memberRowSchema.parse(row); + } + + async findMemberByExternalIdentity( + installationId: string, + provider: string, + subject: string, + ): Promise { + const row = this.#database.prepare(` + SELECT + member.id, + member.installation_id AS installationId, + member.email, + member.display_name AS displayName, + member.role, + member.status, + member.created_at AS createdAt, + member.updated_at AS updatedAt + FROM external_identities AS external + JOIN installation_members AS member ON member.id = external.member_id + WHERE + member.installation_id = ? AND external.provider = ? AND + external.subject = ? + `).get(installationId, provider, subject); + return row === undefined ? null : memberRowSchema.parse(row); + } + async bindExternalIdentity(command: BindExternalIdentityRecord): Promise { try { const result = this.#database.prepare(` diff --git a/tests/conformance/auth-029-identity-repository.test.ts b/tests/conformance/auth-029-identity-repository.test.ts new file mode 100644 index 00000000..e7dce0c8 --- /dev/null +++ b/tests/conformance/auth-029-identity-repository.test.ts @@ -0,0 +1,124 @@ +import {mkdtemp, rm} from "node:fs/promises"; +import {tmpdir} from "node:os"; +import path from "node:path"; + +import {afterEach, beforeEach, describe, expect, test} from "vitest"; + +import {membershipRoles} from "../../src/core/identity.js"; +import {SqliteIdentityRepository} from "../../src/storage/sqlite-identity-repository.js"; + +describe("status-inclusive admission lookups", () => { + let directory: string; + let repository: SqliteIdentityRepository; + + beforeEach(async () => { + directory = await mkdtemp(path.join(tmpdir(), "artifact-admission-identity-")); + repository = new SqliteIdentityRepository(path.join(directory, "identity.db")); + await repository.admitMember({ + createdAt: "2026-10-09T00:00:00.000Z", + displayName: "Admitted member", + email: "member@example.test", + id: "member-one", + installationId: "installation-one", + role: membershipRoles.member, + }); + await repository.bindExternalIdentity({ + boundAt: "2026-10-09T00:00:00.000Z", + email: "member@example.test", + memberId: "member-one", + provider: "oidc:https://issuer-one.example.test", + subject: "subject-one", + }); + }); + + afterEach(async () => { + repository.close(); + await rm(directory, {force: true, recursive: true}); + }); + + test("AUTH-029: email admission lookup returns active and inactive members", async () => { + const active = await repository.findMemberByEmail( + "installation-one", "member@example.test", + ); + expect(active).toMatchObject({id: "member-one", status: "active"}); + + await repository.deactivateMember( + "installation-one", "member-one", "2026-10-09T01:00:00.000Z", + ); + const inactive = await repository.findMemberByEmail( + "installation-one", "member@example.test", + ); + expect(inactive).toMatchObject({ + id: "member-one", + status: "inactive", + updatedAt: "2026-10-09T01:00:00.000Z", + }); + await expect(repository.findActiveMemberByEmail( + "installation-one", "member@example.test", + )).resolves.toBeNull(); + }); + + test("AUTH-029: bound-subject lookup retains an inactive member after email changes", async () => { + const active = await repository.findMemberByExternalIdentity( + "installation-one", "oidc:https://issuer-one.example.test", "subject-one", + ); + expect(active).toMatchObject({id: "member-one", status: "active"}); + + await repository.deactivateMember( + "installation-one", "member-one", "2026-10-09T01:00:00.000Z", + ); + await repository.bindExternalIdentity({ + boundAt: "2026-10-09T02:00:00.000Z", + email: "changed@example.test", + memberId: "member-one", + provider: "oidc:https://issuer-one.example.test", + subject: "subject-one", + }); + const inactive = await repository.findMemberByExternalIdentity( + "installation-one", "oidc:https://issuer-one.example.test", "subject-one", + ); + expect(inactive).toMatchObject({ + email: "member@example.test", id: "member-one", status: "inactive", + }); + await expect(repository.findActiveMemberByExternalIdentity( + "installation-one", "oidc:https://issuer-one.example.test", "subject-one", + )).resolves.toBeNull(); + }); + + test("AUTH-029: email and bound-subject lookups preserve installation and issuer scopes", async () => { + await repository.admitMember({ + createdAt: "2026-10-09T00:00:00.000Z", + displayName: "Other installation member", + email: "member@example.test", + id: "member-two", + installationId: "installation-two", + role: membershipRoles.member, + }); + await repository.bindExternalIdentity({ + boundAt: "2026-10-09T00:00:00.000Z", + email: "member@example.test", + memberId: "member-two", + provider: "oidc:https://issuer-two.example.test", + subject: "subject-one", + }); + + await expect(repository.findMemberByEmail( + "installation-two", "member@example.test", + )).resolves.toMatchObject({id: "member-two", installationId: "installation-two"}); + await expect(repository.findMemberByEmail( + "installation-three", "member@example.test", + )).resolves.toBeNull(); + await expect(repository.findMemberByExternalIdentity( + "installation-two", "oidc:https://issuer-two.example.test", "subject-one", + )).resolves.toMatchObject({id: "member-two", installationId: "installation-two"}); + await expect(repository.findMemberByExternalIdentity( + "installation-one", "oidc:https://issuer-two.example.test", "subject-one", + )).resolves.toBeNull(); + await expect(repository.findMemberByExternalIdentity( + "installation-two", "oidc:https://issuer-one.example.test", "subject-one", + )).resolves.toBeNull(); + await expect(repository.findMemberByExternalIdentity( + "installation-one", "oidc:https://issuer-one.example.test", "missing-subject", + )).resolves.toBeNull(); + }); +}); diff --git a/tests/conformance/oidc-admission-boundary.test.ts b/tests/conformance/oidc-admission-boundary.test.ts new file mode 100644 index 00000000..8207f89f --- /dev/null +++ b/tests/conformance/oidc-admission-boundary.test.ts @@ -0,0 +1,120 @@ +import {mkdtemp, rm} from "node:fs/promises"; +import {tmpdir} from "node:os"; +import path from "node:path"; + +import {Effect, ManagedRuntime, Redacted, Result} from "effect"; +import {afterEach, beforeEach, describe, expect, test} from "vitest"; + +import type {ApplicationRuntime} from "../../src/application/application-runtime.js"; +import {InstallationAccessService} from "../../src/application/installation-access.js"; +import type {ExternalIdentity} from "../../src/core/installation-identity.js"; +import {SystemClock, SystemIdGenerator} from "../../src/core/system.js"; +import {createLocalApplicationLayer} from "../../src/local/create-local-application-layer.js"; +import {LocalBlobStore} from "../../src/storage/local-blob-store.js"; +import {LocalStagingStore} from "../../src/storage/local-staging-store.js"; +import {SqliteArtifactRepository} from "../../src/storage/sqlite-artifact-repository.js"; +import {SqliteIdentityRepository} from "../../src/storage/sqlite-identity-repository.js"; + +const installationId = "oidc-admission-boundary"; +const oidcProvider = "oidc:https://issuer.example.test"; +const memberIdentity: ExternalIdentity = { + displayName: "New member", + email: "member@example.test", + emailVerified: true, + provider: oidcProvider, + subject: "new-member", +}; + +describe("OIDC admission provider and credential boundaries", () => { + let dataDirectory: string; + let repository: SqliteArtifactRepository; + let identityRepository: SqliteIdentityRepository; + let runtime: ApplicationRuntime; + + beforeEach(async () => { + dataDirectory = await mkdtemp(path.join(tmpdir(), "artifact-oidc-boundary-")); + const databasePath = path.join(dataDirectory, "artifact-server.db"); + repository = new SqliteArtifactRepository(databasePath); + identityRepository = new SqliteIdentityRepository(databasePath); + runtime = ManagedRuntime.make(createLocalApplicationLayer({ + apiToken: Redacted.make("test-api-token"), + blobs: new LocalBlobStore(path.join(dataDirectory, "blobs")), + bootstrapAdministratorEmail: "administrator@example.test", + clock: new SystemClock(), + dispatches: repository, + externalApiBearerVerifier: null, + externalIdentityAdmissionPolicy: { + allowedEmailDomains: [], + provider: oidcProvider, + }, + externalMcpBearerVerifier: null, + externalMcpOAuthVerifier: null, + ids: new SystemIdGenerator(), + identityRepository, + installationId, + interactiveIdentityProvider: null, + localBootstrapCredential: null, + protectBootstrapAdministrator: false, + repository, + staging: new LocalStagingStore(path.join(dataDirectory, "staging")), + })); + await runtime.runPromise(InstallationAccessService.use((access) => + access.completeExternalIdentity({ + ...memberIdentity, + email: "administrator@example.test", + subject: "administrator", + }) + )); + }); + + afterEach(async () => { + await runtime.dispose(); + identityRepository.close(); + repository.close(); + await rm(dataDirectory, {force: true, recursive: true}); + }); + + test("AUTH-029: an enabled browser policy cannot admit an unknown bearer identity", async () => { + const denied = await runtime.runPromise(Effect.result( + InstallationAccessService.use((access) => + access.authenticateExternalIdentity(memberIdentity) + ), + )); + expect(Result.match(denied, { + onFailure: (failure) => failure._tag, + onSuccess: () => "UnexpectedSuccess", + })).toBe("IdentityAdmissionDenied"); + expect(await identityRepository.listMembers(installationId)).toHaveLength(1); + expect(await identityRepository.findMemberByExternalIdentity( + installationId, oidcProvider, "new-member", + )).toBeNull(); + + const admitted = await runtime.runPromise(InstallationAccessService.use((access) => + access.completeExternalIdentity(memberIdentity) + )); + const principal = await runtime.runPromise(InstallationAccessService.use((access) => + access.authenticateExternalIdentity(memberIdentity) + )); + expect(principal).toMatchObject({ + id: admitted.session.member.id, + membershipRole: "member", + }); + expect(await identityRepository.listMembers(installationId)).toHaveLength(2); + }); + + test("AUTH-029: an enabled OIDC policy cannot admit an unknown WorkOS browser identity", async () => { + const denied = await runtime.runPromise(Effect.result( + InstallationAccessService.use((access) => + access.completeExternalIdentity({...memberIdentity, provider: "workos"}) + ), + )); + expect(Result.match(denied, { + onFailure: (failure) => failure._tag, + onSuccess: () => "UnexpectedSuccess", + })).toBe("IdentityAdmissionDenied"); + expect(await identityRepository.listMembers(installationId)).toHaveLength(1); + expect(await identityRepository.findMemberByExternalIdentity( + installationId, "workos", "new-member", + )).toBeNull(); + }); +}); diff --git a/tests/conformance/oidc-admission-configuration.test.ts b/tests/conformance/oidc-admission-configuration.test.ts new file mode 100644 index 00000000..5415a516 --- /dev/null +++ b/tests/conformance/oidc-admission-configuration.test.ts @@ -0,0 +1,148 @@ +import {afterEach, describe, expect, test} from "vitest"; + +import { + assertAtMostOneBrowserLoginProvider, + loadOidcConfiguration, +} from "../../src/cli/oidc-configuration.js"; +import {browserLoginKinds, privateTeamBrowserAccess} from + "../../src/core/browser-access.js"; +import {createOidcIdentityProvider} from + "../../src/identity/oidc-identity-provider.js"; +import { + createTestInstallation, + removeTestInstallation, + reserveLoopbackPort, + startTestServer, + type RunningTestServer, + type TestInstallation, +} from "../support/runtime-harness.js"; +import { + startStubOidcLogin, + startStubOidcProvider, + type RunningStubOidcProvider, +} from "../support/stub-oidc-provider.js"; + +const environment = { + ARTIFACT_SERVER_BOOTSTRAP_ADMIN_EMAIL: "administrator@example.test", + ARTIFACT_SERVER_OIDC_CLIENT_ID: "artifact-server", + ARTIFACT_SERVER_OIDC_ISSUER: "https://idp.example.test/dex/", + ARTIFACT_SERVER_ORIGIN: "https://artifacts.example.test", +}; + +describe("OIDC automatic-admission configuration", () => { + let installation: TestInstallation | null = null; + let provider: RunningStubOidcProvider | null = null; + let server: RunningTestServer | null = null; + + afterEach(async () => { + if (server !== null) await server.stop(); + if (provider !== null) await provider.stop(); + if (installation !== null) await removeTestInstallation(installation); + server = null; + provider = null; + installation = null; + }); + + test("AUTH-029: admission defaults off and an enabled policy normalizes its issuer and domains", async () => { + expect(await loadOidcConfiguration({})).toBeNull(); + const configurations = await Promise.all([ + loadOidcConfiguration(environment), + loadOidcConfiguration({ + ...environment, ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "false", + }), + ]); + for (const configuration of configurations) { + expect(configuration).not.toHaveProperty("externalIdentityAdmissionPolicy"); + } + expect(await loadOidcConfiguration({ + ...environment, + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "true", + ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: " Example.COM, EXAMPLE.test ", + })).toMatchObject({ + externalIdentityAdmissionPolicy: { + allowedEmailDomains: ["example.com", "example.test"], + provider: "oidc:https://idp.example.test/dex", + }, + }); + expect(await loadOidcConfiguration({ + ...environment, + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "true", + ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: " ", + })).toMatchObject({ + externalIdentityAdmissionPolicy: {allowedEmailDomains: []}, + }); + }); + + test.each(["TRUE", "False", "1", "yes", " true", ""])( + "AUTH-029: rejects nonliteral auto-admission switch %j", + async (switchValue) => { + await expect(loadOidcConfiguration({ + ...environment, + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: switchValue, + })).rejects.toThrow("ARTIFACT_SERVER_OIDC_AUTO_ADMIT"); + }, + ); + + test.each([ + "*.example.test", "example.test,", ",example.test", "example..test", + "-example.test", "example-.test", "https://example.test", "example.test:443", + "user@example.test", "example_test", "example.test.", "example test", + `${"a".repeat(64)}.test`, + ])("AUTH-029: rejects malformed exact-domain list %j even when disabled", async (domains) => { + await expect(loadOidcConfiguration({ + ...environment, + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "false", + ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: domains, + })).rejects.toThrow("ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS"); + }); + + test.each([ + {ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "false"}, + {ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "true"}, + {ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: ""}, + ])("AUTH-029: policy settings require complete OIDC configuration %j", async (settings) => { + await expect(loadOidcConfiguration(settings)).rejects.toThrow( + "Generic OIDC authentication requires", + ); + expect(() => assertAtMostOneBrowserLoginProvider({ + ARTIFACT_SERVER_WORKOS_CLIENT_ID: "workos-client", + ...settings, + })).toThrow("One installation has one browser-login provider"); + }); + + test("AUTH-029: the loaded policy admits a new member through real browser login", async () => { + installation = await createTestInstallation(); + provider = await startStubOidcProvider({clientId: "artifact-server"}); + const port = await reserveLoopbackPort(); + const configuration = await loadOidcConfiguration({ + ...environment, + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: "true", + ARTIFACT_SERVER_OIDC_ALLOWED_EMAIL_DOMAINS: " EXAMPLE.TEST ", + ARTIFACT_SERVER_OIDC_ISSUER: provider.issuer, + ARTIFACT_SERVER_ORIGIN: `http://127.0.0.1:${port}`, + }); + if (configuration === null) throw new Error("Missing OIDC configuration."); + server = await startTestServer(installation, { + ...configuration, + browserAccess: privateTeamBrowserAccess(browserLoginKinds.oidc), + interactiveIdentityProvider: createOidcIdentityProvider(configuration), + port, + }); + provider.claims.email = "administrator@example.test"; + provider.claims.subject = "administrator"; + expect((await completeCallback(server)).status).toBe(303); + provider.claims.email = "new-member@example.test"; + provider.claims.subject = "new-member"; + expect((await completeCallback(server)).status).toBe(303); + }); +}); + +async function completeCallback( + server: RunningTestServer, +): Promise { + const login = await startStubOidcLogin(server.baseUrl); + return fetch(login.callbackUrl, { + headers: {Cookie: login.handshakeCookie}, + redirect: "manual", + }); +} diff --git a/tests/conformance/oidc-auto-admission.test.ts b/tests/conformance/oidc-auto-admission.test.ts new file mode 100644 index 00000000..5130bf70 --- /dev/null +++ b/tests/conformance/oidc-auto-admission.test.ts @@ -0,0 +1,309 @@ +import path from "node:path"; +import {DatabaseSync} from "node:sqlite"; + +import {Redacted} from "effect"; +import {afterEach, beforeEach, describe, expect, test} from "vitest"; +import {z} from "zod"; + +import {browserLoginKinds, privateTeamBrowserAccess} from "../../src/core/browser-access.js"; +import {createOidcIdentityProvider} from "../../src/identity/oidc-identity-provider.js"; +import { + createTestInstallation, + removeTestInstallation, + reserveLoopbackPort, + startTestServer, + type RunningTestServer, + type TestInstallation, +} from "../support/runtime-harness.js"; +import { + startStubOidcLogin, + startStubOidcProvider, + type RunningStubOidcProvider, +} from "../support/stub-oidc-provider.js"; + +const administratorEmail = "administrator@example.test"; +const clientSecret = "oidc-admission-test-client-secret"; +const sessionSchema = z.object({ + principal: z.object({id: z.string(), membershipRole: z.enum(["administrator", "member"])}), +}); +const memberSchema = z.object({member: z.object({id: z.string()})}); +const countSchema = z.object({total: z.number()}); + +interface ApplicationCookies { + readonly csrf: string; + readonly header: string; +} + +describe("OIDC automatic browser admission", () => { + let installation: TestInstallation; + let provider: RunningStubOidcProvider; + let server: RunningTestServer | null = null; + + beforeEach(async () => { + installation = await createTestInstallation(); + provider = await startStubOidcProvider({clientId: "oidc-admission-test", clientSecret}); + }); + + afterEach(async () => { + if (server !== null) await server.stop(); + await provider.stop(); + await removeTestInstallation(installation); + server = null; + }); + + async function startServer( + allowedEmailDomains: readonly string[] | null = [], + policyProvider = `oidc:${provider.issuer}`, + ): Promise { + const port = await reserveLoopbackPort(); + const applicationOrigin = `http://127.0.0.1:${port}`; + const options = { + applicationOrigin, + bootstrapAdministratorEmail: administratorEmail, + browserAccess: privateTeamBrowserAccess(browserLoginKinds.oidc), + interactiveIdentityProvider: createOidcIdentityProvider({ + applicationOrigin, + clientId: provider.clientId, + clientSecret: Redacted.make(clientSecret), + issuer: provider.issuer, + scopes: "openid email profile", + }), + port, + }; + if (allowedEmailDomains !== null) { + Object.assign(options, {externalIdentityAdmissionPolicy: {provider: policyProvider, allowedEmailDomains}}); + } + server = await startTestServer(installation, options); + return server; + } + + async function bootstrap(running: RunningTestServer): Promise { + provider.claims.email = administratorEmail; + provider.claims.subject = "administrator-subject"; + provider.claims.emailVerified = true; + const completed = await login(running); + expect(completed.status).toBe(303); + return applicationCookies(completed); + } + + async function principal(running: RunningTestServer, response: Response) { + const session = await fetch(`${running.baseUrl}/api/v1/session`, { + headers: {Cookie: applicationCookies(response).header}, + }); + expect(session.status).toBe(200); + return sessionSchema.parse(await session.json()).principal; + } + + test("AUTH-029-F: an unadmitted OIDC user remains denied by default", async () => { + const running = await startServer(null); + await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + const denied = await login(running); + expect(denied.status).toBe(403); + expect(denied.headers.getSetCookie()).toEqual([]); + expect(rowCount(installation, "installation_members")).toBe(1); + }); + + test("AUTH-029-B: enabled browser admission creates a normal member and reuses its identity", async () => { + const running = await startServer(); + await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + const admitted = await login(running); + expect(admitted.status).toBe(303); + const first = await principal(running, admitted); + expect(first.membershipRole).toBe("member"); + const repeated = await login(running); + expect(repeated.status).toBe(303); + expect(await principal(running, repeated)).toEqual(first); + expect(rowCount(installation, "installation_members")).toBe(2); + expect(rowCount(installation, "external_identities")).toBe(2); + }); + + test("AUTH-029: automatic admission preserves first-administrator bootstrap", async () => { + const running = await startServer(); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + expect((await login(running)).status).toBe(403); + expect(rowCount(installation, "installation_members")).toBe(0); + const cookies = await bootstrap(running); + const session = await fetch(`${running.baseUrl}/api/v1/session`, {headers: {Cookie: cookies.header}}); + expect(sessionSchema.parse(await session.json()).principal.membershipRole).toBe("administrator"); + }); + + test.each([ + {email: "member@ackstorm.com", subject: "different-subject"}, + {email: "changed@ackstorm.com", subject: "member-subject"}, + {email: "other@ackstorm.com", subject: "member-subject"}, + ])("AUTH-029: inactive identity stays denied with $email and $subject", async ({email, subject}) => { + const running = await startServer(); + const adminCookies = await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + const admitted = await login(running); + expect(admitted.status).toBe(303); + const member = await principal(running, admitted); + const other = await fetch(`${running.baseUrl}/api/v1/members`, { + body: JSON.stringify({displayName: "Other", email: "other@ackstorm.com", role: "member"}), + headers: mutationHeaders(running.baseUrl, adminCookies), + method: "POST", + }); + expect(other.status).toBe(201); + const deactivated = await fetch(`${running.baseUrl}/api/v1/members/${member.id}/deactivate`, { + headers: mutationHeaders(running.baseUrl, adminCookies), method: "POST", + }); + expect(deactivated.status).toBe(200); + const sessionCount = rowCount(installation, "application_sessions"); + provider.claims.email = email; + provider.claims.subject = subject; + const denied = await login(running); + expect(denied.status).toBe(403); + expect(denied.headers.getSetCookie()).toEqual([]); + expect(rowCount(installation, "installation_members")).toBe(3); + expect(rowCount(installation, "application_sessions")).toBe(sessionCount); + const revoked = await fetch(`${running.baseUrl}/api/v1/session`, { + headers: {Cookie: applicationCookies(admitted).header}, + }); + expect(revoked.status).toBe(401); + }); + + test("AUTH-029: simultaneous first logins converge on one member", async () => { + const running = await startServer(); + await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + const attempts = await Promise.all([ + startStubOidcLogin(running.baseUrl), startStubOidcLogin(running.baseUrl), + ]); + const completed = await Promise.all(attempts.map((attempt) => fetch(attempt.callbackUrl, { + headers: {Cookie: attempt.handshakeCookie}, redirect: "manual", + }))); + expect(completed.map((response) => response.status)).toEqual([303, 303]); + const principals = await Promise.all(completed.map((response) => principal(running, response))); + expect(principals[0]).toEqual(principals[1]); + expect(principals[0]?.membershipRole).toBe("member"); + expect(rowCount(installation, "installation_members")).toBe(2); + expect(rowCount(installation, "external_identities")).toBe(2); + }); + + test("AUTH-029: enabled admission still refuses an explicitly unverified email", async () => { + const running = await startServer(); + await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + provider.claims.emailVerified = false; + const denied = await login(running); + expect(denied.status).toBe(403); + expect(denied.headers.getSetCookie()).toEqual([]); + expect(rowCount(installation, "installation_members")).toBe(1); + }); + + test("AUTH-029: a policy for another issuer cannot admit a verified user", async () => { + const running = await startServer([], "oidc:https://another-issuer.example.test"); + await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + expect((await login(running)).status).toBe(403); + expect(rowCount(installation, "installation_members")).toBe(1); + }); + + test("AUTH-029: a token with a foreign signature creates no member or session", async () => { + const running = await startServer(); + await bootstrap(running); + provider.claims.email = "member@ackstorm.com"; + provider.claims.subject = "member-subject"; + provider.defects.signWithForeignKey = true; + const denied = await login(running); + expect(denied.status).toBe(502); + expect(denied.headers.getSetCookie()).toEqual([]); + expect(rowCount(installation, "installation_members")).toBe(1); + expect(rowCount(installation, "application_sessions")).toBe(1); + }); + + test("AUTH-029: explicitly admitted administrators keep their role", async () => { + const running = await startServer(); + const cookies = await bootstrap(running); + const admitted = await fetch(`${running.baseUrl}/api/v1/members`, { + body: JSON.stringify({displayName: "Second admin", email: "admin@ackstorm.com", role: "administrator"}), + headers: mutationHeaders(running.baseUrl, cookies), method: "POST", + }); + expect(admitted.status).toBe(201); + const member = memberSchema.parse(await admitted.json()).member; + provider.claims.email = "admin@ackstorm.com"; + provider.claims.subject = "second-admin-subject"; + const completed = await login(running); + expect(completed.status).toBe(303); + expect(await principal(running, completed)).toEqual({id: member.id, membershipRole: "administrator"}); + }); + + test("AUTH-029: a listed domain admits a member after email normalization", async () => { + const running = await startServer(["ackstorm.com"]); + await bootstrap(running); + provider.claims.email = "Member@ACKSTORM.COM"; + provider.claims.subject = "domain-member-subject"; + const completed = await login(running); + expect(completed.status).toBe(303); + expect((await principal(running, completed)).membershipRole).toBe("member"); + expect(rowCount(installation, "installation_members")).toBe(2); + }); + + test.each(["member@gmail.com", "member@sub.ackstorm.com", "member@ackstorm.com.attacker.test"])( + "AUTH-029: domain allowlisting refuses %s", async (email) => { + const running = await startServer(["ackstorm.com"]); + await bootstrap(running); + provider.claims.email = email; + provider.claims.subject = "domain-member-subject"; + const denied = await login(running); + expect(denied.status).toBe(403); + expect(denied.headers.getSetCookie()).toEqual([]); + expect(rowCount(installation, "installation_members")).toBe(1); + expect(rowCount(installation, "application_sessions")).toBe(1); + }, + ); + + test("AUTH-029: the domain allowlist preserves explicit admission outside its domains", async () => { + const running = await startServer(["ackstorm.com"]); + const cookies = await bootstrap(running); + const admitted = await fetch(`${running.baseUrl}/api/v1/members`, { + body: JSON.stringify({displayName: "Invited", email: "invited@example.net", role: "member"}), + headers: mutationHeaders(running.baseUrl, cookies), method: "POST", + }); + expect(admitted.status).toBe(201); + provider.claims.email = "invited@example.net"; + provider.claims.subject = "invited-subject"; + const completed = await login(running); + expect(completed.status).toBe(303); + expect((await principal(running, completed)).membershipRole).toBe("member"); + }); +}); + +async function login(running: RunningTestServer): Promise { + const started = await startStubOidcLogin(running.baseUrl); + return fetch(started.callbackUrl, {headers: {Cookie: started.handshakeCookie}, redirect: "manual"}); +} + +function applicationCookies(response: Response): ApplicationCookies { + const pairs = response.headers.getSetCookie() + .filter((value) => value.startsWith("artifact_session=") || value.startsWith("artifact_csrf=")) + .map((value) => value.split(";", 1)[0]); + const csrfPair = pairs.find((value) => value?.startsWith("artifact_csrf=")); + if (pairs.length !== 2 || csrfPair === undefined) throw new Error("The response lacks application cookies."); + return {header: pairs.join("; "), csrf: csrfPair.slice("artifact_csrf=".length)}; +} + +function mutationHeaders(origin: string, cookies: ApplicationCookies): Headers { + return new Headers({ + "Content-Type": "application/json", Cookie: cookies.header, Origin: origin, + "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "X-CSRF-Token": cookies.csrf, + }); +} + +function rowCount(installation: TestInstallation, table: string): number { + const database = new DatabaseSync(path.join(installation.dataDirectory, "artifact-server.db"), {readOnly: true}); + try { + return countSchema.parse(database.prepare(`SELECT COUNT(*) AS total FROM ${table}`).get()).total; + } finally { + database.close(); + } +} diff --git a/tests/integration/external-storage-runtime.test.ts b/tests/integration/external-storage-runtime.test.ts index e6f3fd25..ea027367 100644 --- a/tests/integration/external-storage-runtime.test.ts +++ b/tests/integration/external-storage-runtime.test.ts @@ -40,6 +40,7 @@ import type {GitHistoryProviderHealthProbe} from "../../src/git-history/git-history-provider-health.js"; import {managedApiKeyCredentialPattern} from "../../src/core/installation-identity.js"; +import type {ExternalIdentityAdmissionPolicy} from "../../src/core/installation-identity.js"; import { browserLoginKinds, privateTeamBrowserAccess, @@ -1027,6 +1028,70 @@ describe.sequential("external-storage Postgres and S3 runtime", () => { } }); + test("AUTH-029: OIDC admission converges across Postgres replicas and refuses inactive subjects", async () => { + const previousClaims = {...oidcProvider.claims}; + const identity = { + apiToken: managedTestKey("oidc-auto-admission"), + installationId: "external-storage-oidc-auto-admission", + autoAdmit: true, + }; + const [first, second] = await Promise.all([ + startExternalStorageProcess(environment, identity), + startInProcessExternalStorageServer(environment, identity, { + externalIdentityAdmissionPolicy: {provider: `oidc:${oidcProvider.issuer}`, allowedEmailDomains: []}, + }), + ]); + try { + oidcProvider.claims.email = oidcAdministratorEmail; + oidcProvider.claims.subject = "postgres-admission-administrator"; + oidcProvider.claims.emailVerified = true; + const administrator = await oidcBrowserLogin(first.baseUrl); + expect(administrator.status).toBe(303); + const adminCookies = applicationCookies(administrator.headers.getSetCookie()); + oidcProvider.claims.email = "member@ackstorm.com"; + oidcProvider.claims.subject = "postgres-admission-member"; + const responses = await Promise.all(Array.from({length: 12}, (_, index) => + oidcBrowserLogin(index % 2 === 0 ? first.baseUrl : second.baseUrl) + )); + expect(responses.map((response) => response.status)).toEqual(Array(12).fill(303)); + const members = await Promise.all(responses.map(async (response) => { + const session = await fetch(`${second.baseUrl}/api/v1/session`, { + headers: {Cookie: applicationCookies(response.headers.getSetCookie()).header}, + }); + expect(session.status).toBe(200); + return z.object({principal: z.object({id: z.string(), membershipRole: z.literal("member")})}) + .parse(await session.json()).principal; + })); + expect(members.every((member) => member.id === members[0]?.id)).toBe(true); + const member = members[0]; + if (member === undefined) throw new Error("No member was admitted."); + const listed = await fetch(`${first.baseUrl}/api/v1/members`, {headers: {Cookie: adminCookies.header}}); + expect(z.object({members: z.array(z.object({id: z.string()}))}).parse(await listed.json()).members) + .toHaveLength(2); + const deactivated = await fetch(`${first.baseUrl}/api/v1/members/${member.id}/deactivate`, { + headers: browserMutationHeaders("https://artifacts.example.com", adminCookies), method: "POST", + }); + expect(deactivated.status).toBe(200); + oidcProvider.claims.email = "changed@ackstorm.com"; + const denied = await oidcBrowserLogin(second.baseUrl); + expect(denied.status).toBe(403); + expect(denied.headers.getSetCookie()).toEqual([]); + const database = await PostgresDatabase.open({maxConnections: 2, url: Redacted.make(environment.databaseUrl)}, "validate"); + try { + const repository = new PostgresIdentityRepository(database, identity.installationId); + expect(await repository.findMemberByEmail(identity.installationId, "member@ackstorm.com")) + .toMatchObject({id: member.id, status: "inactive"}); + expect(await repository.findMemberByExternalIdentity(identity.installationId, `oidc:${oidcProvider.issuer}`, "postgres-admission-member")) + .toMatchObject({id: member.id, status: "inactive"}); + expect(await repository.listMembers(identity.installationId)).toHaveLength(2); + } finally { + await database.close(); + } + } finally { + Object.assign(oidcProvider.claims, previousClaims); + } + }); + test("AUTH-009-B AUTH-009-F: sessions, managed keys, and staged uploads cross process boundaries", async () => { expect.hasAssertions(); const externalStorageIdentity = { @@ -2572,6 +2637,7 @@ function startExternalStorageProcess( readonly apiToken: string; readonly installationId: string; readonly storageBucket?: string; + readonly autoAdmit?: boolean; }, ): Promise { const child = spawn( @@ -2581,6 +2647,7 @@ function startExternalStorageProcess( cwd: repositoryRoot, env: { ...process.env, + ARTIFACT_SERVER_OIDC_AUTO_ADMIT: identity.autoAdmit === true ? "true" : "false", ARTIFACT_SERVER_API_TOKEN: identity.apiToken, ARTIFACT_SERVER_BOOTSTRAP_ADMIN_EMAIL: oidcAdministratorEmail, ARTIFACT_SERVER_CONTENT_DOMAIN: "content.example.net", @@ -2631,6 +2698,7 @@ async function startInProcessExternalStorageServer( options: { readonly gitHistory?: NodeGitHistoryConfiguration; readonly gitHistoryHealthProbe?: GitHistoryProviderHealthProbe; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; } = {}, ): Promise { const server = await startExternalStorageServer({ diff --git a/tests/support/runtime-harness.ts b/tests/support/runtime-harness.ts index 328c4acc..b66e3b9f 100644 --- a/tests/support/runtime-harness.ts +++ b/tests/support/runtime-harness.ts @@ -26,6 +26,7 @@ import type { McpOAuthResourceConfiguration, } from "../../src/http/create-http-app.js"; import type {Clock} from "../../src/core/ports.js"; +import type {ExternalIdentityAdmissionPolicy} from "../../src/core/installation-identity.js"; import {localOwnerBrowserAccess} from "../../src/core/browser-access.js"; import type {BrowserAccess} from "../../src/core/browser-access.js"; import {defaultCompletedRequestLogSampleRate} from @@ -93,6 +94,7 @@ export async function startTestServer( readonly externalApiBearerVerifier?: BearerCredentialVerifier; readonly externalMcpBearerVerifier?: BearerCredentialVerifier; readonly externalMcpOAuthVerifier?: ExternalMcpBearerVerifier; + readonly externalIdentityAdmissionPolicy?: ExternalIdentityAdmissionPolicy; readonly gitHistory?: NodeGitHistoryConfiguration; readonly gitHistoryHealthProbe?: GitHistoryProviderHealthProbe; readonly gitHistoryProvider?: GitHistoryProvider; @@ -121,6 +123,9 @@ export async function startTestServer( port: options.port ?? 0, }; let config: LocalServerConfig = baseConfig; + if (options.externalIdentityAdmissionPolicy !== undefined) { + config = {...config, externalIdentityAdmissionPolicy: options.externalIdentityAdmissionPolicy}; + } if (options.applicationOrigin !== undefined) { config = {...config, applicationOrigin: options.applicationOrigin}; }