mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 05:56:19 +01:00
Reconcile campaign metadata and preserve startup diagnosis
This commit is contained in:
1 parent
05b29a381c
commit
819c1622d1
14 files changed
+2982
-448
No files matched your search
@@ -5,9 +5,10 @@ Updated: 2026-10-26. This is the single progress record for [the saved goal](202
|
||||
## Current state
|
||||
|
||||
- **244 scenarios / 35 areas / 191 capabilities**, frozen inventory digest `9dc9cf6a5426cf1b56bc30a3571851f233f9dd5e93a1292eaee414d33b52df77`. All original queries, contexts and expected outcomes remain unchanged. **196 passes required**; blocked, partial, unsupported and unrun remain in the denominator.
|
||||
- **134 authored: 129 without declared coverage gaps, 5 partial, 110 unimplemented** at committed-source metadata checkpoint `ad0907afb9ca7b6d528d90616c3a12b52c94620c`. There are 132 concrete lazy descriptors plus two legacy task cases. All callbacks, bounds, policies, source dependencies and four browser boundaries were reconciled from this one Git archive; uncommitted or newer identity/integrator work is excluded. These are authoring counts, not acceptance.
|
||||
- **Three scenarios attempted; four actual measurements; zero first-attempt passes and zero final passes (0/244 for both rates).** Goal-tree is exhausted after two failures. Alignment and purpose each retain one failed initial attempt and one allowed correction/rerun remaining.
|
||||
- **No live launcher, browser, database or provider process is running.** Manager held the two final retries before reservation, so remaining shared authoring contracts cannot stale a pass after its final allowed attempt. No unchanged rerun, extra nudge, deadline extension, automatic retry or relabeled canonical health probe was used.
|
||||
- **207 authored: 198 without declared coverage gaps, 9 partial, 37 unimplemented** at exact committed-source metadata checkpoint `e4b62f48aacef0bef16fd1f5a28433a8192af8ba`. There are 205 concrete lazy descriptors plus two legacy task cases. All actual callbacks, bounds, policies and source dependencies were reconciled from this one Git archive; later external/runtime work remains pending. These are authoring counts, not acceptance.
|
||||
- Five partial definitions contain concrete executable supported-surface/context blocker probes: two access grants, subscription provenance, skill-use authority/context and genuine prior-day routine history. Four have unfinished required branches: ask-before-action, named ChatGPT setup, team installer failures and native archive-fault launcher transport. **203 authoring-resolved; 4 unfinished; 37 absent.** Only actual reserved execution can process a blocker; offline probes/calibrations do not establish processing. Incomplete conditions cannot pass.
|
||||
- **Four scenarios attempted; five actual measurements; zero first-attempt passes and zero final passes (0/244 for both rates).** Goal-tree is exhausted after two failures. Alignment and purpose each retain one failed initial attempt and one allowed correction/rerun remaining.
|
||||
- **No live launcher or provider process is running.** Admissions stopped after the authorized sign-in initial failed PostgreSQL bootstrap and finalization; its recovery state remains private with cleanup unconfirmed. The other four authorized initial cases remain unreserved. Final retries remain held for coherent B. No unchanged rerun, extra nudge, deadline extension, automatic retry or relabeled canonical health probe was used.
|
||||
- Production baseline `8373d7867963674ff960d2345d0d28086f799bc7`; branch `codex/chat-catalog-evals`. PR #17794 merged via `2ee36a4`. No eval/core PR has been created. No merge is eligible.
|
||||
|
||||
## Measured evidence — all failures retained
|
||||
@@ -47,15 +48,15 @@ The finite assignments cover the full denominator; future-wave assignments remai
|
||||
| `eval_external` | 52 | connection-setup, app-permissions, chat-channels, assistant-connections, gateways-profiles, plugins, teams, secrets |
|
||||
| `eval_campaign` | 16 | instance-operations, cli-operations, developer-labs; also sole live coordinator and ledger/report owner |
|
||||
|
||||
Next: finish remaining shared/area authoring contracts before immutable freeze A, then run remaining initial attempts sequentially with retries zero. Accumulate one coherent correction set, freeze B, and review the exact eligible final retry set before execution. Alignment and purpose final retries stay deferred into B; goal-tree is exhausted and must not receive a third run. The coordinator pauses after this finite checkpoint for the external authoring slot and retains sole live/ledger ownership. No live test suites run concurrently with campaign instances. Preflight rechecks IPC capacity and exact source/binary/dependency identity; no arbitrary IPC cleanup, kernel tuning or live database changes.
|
||||
Next: manager review of a bounded initial-only slice from an immutable snapshot of this checkpoint; remaining area authoring continues. Run each approved initial sequentially with retries zero, fresh owned environment and cleanup. Final retries remain deferred until coherent freeze B after all initial processing. Accumulate one coherent correction set, freeze B, and review the exact eligible final retry set before execution. Alignment and purpose final retries stay deferred into B; goal-tree is exhausted and must not receive a third run. The coordinator retains sole live/ledger ownership. No new reservation or launch is authorized by this metadata checkpoint. No live test suites run concurrently with campaign instances. Preflight rechecks IPC capacity and exact source/binary/dependency identity; no arbitrary IPC cleanup, kernel tuning or live database changes.
|
||||
|
||||
Local tools/dependencies/private subscription auth file are present. Earlier absence described exported provider-key variables and `.env.runner-e2e.local`, not absence from credential sources. Exact standard `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENROUTER_API_KEY` and `DAYTONA_API_KEY` names are present in the `~/.secrets` file. Source reference availability does not establish validity or a disposable target; immutable cloud image/resource scope remains unverified. External provider/configuration blockers remain explicit and never passing coverage; simulations cannot establish live provider acceptance. No private artifacts have been published.
|
||||
|
||||
Identity and domain probe helpers now retain one attempt-private credential lifetime with fresh reads and explicit revocation. Remaining external review: allow harmless secret-related artifacts only after actual private byte scanning, and compare secret version/identity semantics rather than JSON property order. Named ChatGPT setup is explicitly partial: real generic OAuth/MCP client consent is retained but cannot prove the named ChatGPT human setup path.
|
||||
|
||||
Core behavior fixes require separate focused PRs that remain unmerged; their dependent passes cannot count toward the eval threshold. Eval merge requires every scenario implemented/processed, at least 196 final eligible passes, latest-head checks/reviews and actual protected merge. Per-case fingerprints, global suite hash and production/source revisions remain distinct; stale passes cannot be carried across changed execution/grader dependencies.
|
||||
Core behavior fixes require separate focused PRs that remain unmerged; their dependent passes cannot count toward the eval threshold. Eval merge requires all 244 authoring-resolved and actually processed (complete implementations or concrete executable blocker probes, with no unfinished branches), at least 196 final eligible passes, latest-head checks/reviews and actual protected merge. Per-case fingerprints, global suite hash and production/source revisions remain distinct; stale passes cannot be carried across changed execution/grader dependencies.
|
||||
|
||||
Credential discovery: relevant source metadata is readable through the existing 1Password service account outside the sandbox. Notion, QA AgentMail, GitHub, Daytona and Anthropic references are available; no matching Slack reference was found. Credential validity, a disposable repository allowlist, designated mail recipient and pinned cloud image remain unverified. Automatic approval review rejected credentialed provider identity/resource discovery and metadata persistence; explicit approval for the narrow read-only AgentMail/GitHub endpoints is pending. No provider discovery request or metadata write occurred. Private item IDs and account metadata remain outside tracked documents.
|
||||
Credential discovery: relevant source metadata is readable through the existing 1Password service account outside the sandbox. Standard source references are available; this does not prove account validity. The user explicitly approved the narrow read-only AgentMail account/inbox and GitHub account/repository metadata discovery, and the external owner completed it. No contents, sends or provider writes were authorized by that discovery. No designated disposable self-mail recipient/repository scope is selected yet. Private item IDs and account metadata remain outside tracked documents.
|
||||
|
||||
## Historical authoring handoff at b33f3b8b64
|
||||
|
||||
@@ -117,8 +118,64 @@ These committed area handoffs await the coordinator's next immutable reconciliat
|
||||
| Coordinator privileged | 16 | 16 | 0 | 0 |
|
||||
| Total pending reconciliation | 185 | 176 | 9 | 59 |
|
||||
|
||||
Integrator's latest finite area handoff is `37b72e28e3` (genuine SDK-managed routine), following `2d53c3952f` / `6cc0012da0` (actual Morgan primary execution and review handoff) and `fe05c54c11` / `edbf426db3` (partial skill enablement). Its five absent IDs are `cases.update-case-facts`, `cases.case-history`, `cases.close-case`, `routines.webhook-trigger`, and `skills.review-update`. Card explanation still lacks the actual responding-principal inaccessible-source phase; card refresh still lacks an actual persisted failed-generation phase retaining prior output. Skill enablement has concrete intended assignment/future-version callbacks but requires an observed public configuration grant and an applicable effective-denial branch; rejection of the nonexistent `skills.use` action alone is not proof that every denial context is unsupported. Partial authoring and unsupported prerequisite receipts never count as passing outcomes or complete inventory processing.
|
||||
Integrator's latest finite area handoff is `37b72e28e3` (genuine SDK-managed routine), following `2d53c3952f` / `6cc0012da0` (actual Morgan primary execution and review handoff) and `fe05c54c11` / `edbf426db3` (partial skill enablement). Its five absent IDs are `cases.update-case-facts`, `cases.case-history`, `cases.close-case`, `routines.webhook-trigger`, and `skills.review-update`. Card explanation still lacks the actual responding-principal inaccessible-source phase; card refresh still lacks an actual persisted failed-generation phase retaining prior output. Skill enablement has concrete intended assignment/future-version callbacks but requires an observed public configuration grant and an applicable effective-denial branch; rejection of the nonexistent `skills.use` action alone is not proof that every denial context is unsupported. Unfinished authoring and source-only/calibration prerequisite receipts never count as passing outcomes or complete inventory processing.
|
||||
|
||||
Shared privacy changes `b72f0f7cbb`, `4e73b7bb70`, `69b2e2b1a2`, and `d1fa9efdaf` protect diagnostic streaming, generated secret registration, reporter archives, authenticated API cookies, and sensitive PATCH calls. Worker registrations synchronously update an owned atomic mode0600 registry outside retained evidence; the launcher redacts before console/log emission and deletes that registry unconditionally. Diagnostic sink cleanup runs after independent observations: actual product response/work-product bytes remain available to leak oracles, while leaking opaque HTML/blob reports are omitted with explicit failure metadata and the original failed diagnosis retained. Actual offline Playwright canaries used synthetic values only, with no provider calls or canonical attempts. Closed source/hash/count receipts are retained under ignored `tests/runner-e2e/results/chat-catalog-campaign/privacy/`: `external-original-closed.json`, `shared-diagnostic-boundary.json`, `identity-private-browser-closed.json`, and `authenticated-api-transport-closed.json`. Raw canary reports are not published. Identity's finite child and exact state-file installation proof scanned 17 sinks without findings; the authenticated API canary scanned nine sinks without findings. Shared fingerprints change with these inputs; older passes cannot silently carry forward.
|
||||
|
||||
`7154ba9e85` adds only the approved numeric GitHub alias/discovery bindings for the two workspace repository scenarios and removes the erroneous requirement for an already-qualified managed Anthropic profile. Source credential-name availability still does not establish validity or target authorization. Narrow authenticated AgentMail/GitHub resource discovery remains pending explicit approval after automatic review rejected it; no credentialed discovery calls or metadata persistence occurred. No production fix, core PR, live launch, reservation, or final retry was added by these authoring handoffs. The coordinator retains sole ledger/execution ownership and resumes sole progress-document ownership after this bounded update.
|
||||
|
||||
## Finite coordinator checkpoint at 994897f542
|
||||
|
||||
Archived source contains 200 authored scenarios: domains 39 plus two legacy tasks, identity 44, runtime 43, integrator 26 and external 30, plus 16 privileged scenarios. Frozen catalog remains 244/35/191 with its original digest. Source production paths and lockfile have no diff from baseline `8373d7867963674ff960d2345d0d28086f799bc7`.
|
||||
|
||||
Validation: all 198 lazy exports imported through actual selected dispatch and matched their metadata (193 ordinary/companion definitions and five browser boundaries); derived `--list` contains 200 cells. Full archived E2E typecheck passed. Archived input/fingerprint/diagnostic/oracle controls passed 20 tests; coordinator ledger controls passed 12 tests. These checks launch no canonical workflow or provider. TSX CLI IPC was sandbox-denied during discovery; the supported Node import loader completed the same listing without its auxiliary IPC server.
|
||||
|
||||
Ledger processing is verified against retained terminal `result.json` execution/source identity, not mere reservations: three scenarios processed, four measurements retained, zero eligible passes. Source-chat human counts exclude separately evidenced auxiliary human task comments/operator actions. Imported synthetic fixture dollars never enter campaign/provider billing; actual heartbeat usage remains authoritative.
|
||||
|
||||
Selected semantic private expectations now use an opaque keyed commitment, separate from credential references. Audited Notion fields are `apiVersion`, `olderFactJson`, `olderMarker`, `recentFactJson`, `recentMarker`. The stable random 32-byte mode-0600 campaign key was explicitly initialized once outside tracked artifacts and model inputs. No raw expectations, key bytes or low-entropy hashes are public; missing/replaced key or changed expectations invalidates replay. Case-only missing inputs are sanitized setup blockers inside reserved attempts; profile/environment credentials remain launch prerequisites. No private provider discovery occurred during this checkpoint.
|
||||
|
||||
Proposed manager-reviewed initial slice (all ordinal 1, still unreserved): `access.sign-in` (browser boundary, zero native/source-chat human turns); `projects.create-project` (two native/two source-chat human turns); `tasks.comment-with-reference`, `navigation-preferences.broken-link`, `navigation-preferences.edit-profile` (one native/one source-chat human turn each). All use local Codex subscription profile, max parallelism one and zero retries; no private oracle expectations are selected. Exact candidate source/build/fingerprint receipts must be fixed before admission. Published operator source/target release identities remain distinct from ordinary source-baseline evidence.
|
||||
|
||||
Read-only IPC preflight found 97 free semaphores and 19 free shared-memory slots; the single-instance reserve guard passed. Recheck immediately before admission. No IPC cleanup, kernel tuning, live database access or canonical health probe occurred.
|
||||
|
||||
## Initial slice stopped after access.sign-in
|
||||
|
||||
Manager authorized five initials from isolated commit `0fdcb539a832684ab03a55cc3342c0bef4a9a41a`, based on `994897f542`. The candidate verified 12,465 Git blobs and 5,191 dependency links, with no mutable escapes, unchanged production sources/lock, pinned source-built native binary, full E2E typecheck and 32 focused controls. Native/source/build receipts are retained under `results/chat-catalog-campaign/`.
|
||||
|
||||
Only `access.sign-in` ordinal 1 was reserved and actually launched, campaign `local-2026-10-27T04-01-47-918Z`, launcher PID 86819 (now dead). It failed at embedded PostgreSQL `initdb` bootstrap, before the webserver/browser journey or native/model admission. The original actual fallback result is retained verbatim under `results/local-2026-10-27T04-01-47-918Z/abnormal-finalization/access.sign-in/result.json`; its sidecar explicitly labels postprocessing, the original result/log hashes and the secondary finalization defect. This is one failed measurement, not an uncounted probe. Zero native runs/tokens were admitted; provider dollars remain unpriced.
|
||||
|
||||
Underlying PostgreSQL stderr was not captured: the pinned `embedded-postgres` initialise implementation wires only stdout despite its stderr comment. The retained log proves bootstrap exit 1, not its precise resource cause. Pre- and post-failure headroom remained 97 semaphores/19 shared-memory slots; no IPC exhaustion claim or limit change is justified. No arbitrary cleanup or PostgreSQL retry was performed.
|
||||
|
||||
Secondary harness failure: launcher credentials were replaced by expanded diagnostic variants; the reporter expanded those variants again, incorrectly indexing JSON structural punctuation. Reporter `.last-run.json` became invalid JSON and evidence packaging aborted. The original case-private fallback result survived; original campaign reports zero packaged results. A narrow offline correction retains raw registered secrets, makes repeated expansion stable, serializes reporter JSON structurally, and preserves primary failure plus abnormal-finalization error in a failed result. Synthetic controls cover real failing child stderr, short multiline secrets, repeated synthetic auth/reporter processing, valid JSON and preserved failure attribution. No corrective scenario run occurred.
|
||||
|
||||
Initial cleanup remains **unconfirmed/failed** in the ledger. A later read-only exact-owned-root process scan found zero matches and the launcher dead; this is appended verification, not proof of complete resource cleanup. The original private recovery directory remains retained. The other four cases (`projects.create-project`, `tasks.comment-with-reference`, `navigation-preferences.broken-link`, `navigation-preferences.edit-profile`) remain unreserved and unrun. No final retry was consumed; sign-in's one permitted correction belongs to B.
|
||||
|
||||
Next safe step is a source-qualified test-scoped PostgreSQL diagnostic path that captures the real owned initdb stderr, plus independently verified cleanup and a meaningful environment/correction change. No further canonical admission is authorized until manager review. A production/dependency stderr behavior fix, if required, stays in the separate core lane; no production code was changed here.
|
||||
|
||||
## One infrastructure-only bootstrap diagnostic
|
||||
|
||||
Manager authorized one separate owned `initdb` diagnostic with private stderr capture, after five offline wrong-binary/root/owner/privacy/process-guard controls and fresh read-only IPC/ownership checks passed. No scenario, browser, provider or native runner was admitted; campaign attempt counts remain four scenarios/five measurements/zero eligible passes. Original sign-in and goal budgets are unchanged; the other four initial cases remain unreserved.
|
||||
|
||||
The exact immutable embedded binary was `@embedded-postgres/darwin-arm64` 18.1.0-beta.16, SHA-256 `5a6ad92c2ce915b1a13aa2f8f2b2199996daf0cbe6b9f1d0044a387303b368ec`. Its bytes matched the prior immutable dependency copy. Command matched the failed server's password auth, paperclip user, C locale and UTF-8 flags, against a new mode-0700 owned directory with mode-0600 password/stderr files. No production code, arguments, requests or kernel settings were rewritten.
|
||||
|
||||
Captured stderr proves **semaphore resource exhaustion**: PostgreSQL reported `FATAL: could not create semaphores: No space left on device`, with a failed `semget` allocation of 17 members and an explicit SEMMNI/SEMMNS (not disk-space) explanation. Before and after, host capacity was 97 free semaphores/19 shared-memory slots. The existing 34-semaphore preflight reserve therefore failed to establish bootstrap capacity; no guessed replacement threshold was adopted. The original scenario's missing stderr was not retrospectively fabricated: this is separate diagnostic evidence of the current environment.
|
||||
|
||||
The diagnostic's strict process-group ownership guard rejected a member after bootstrap admission. It captured stderr but raised before waiting, so the bootstrap exit code is **unknown**; controller exit code 1 is recorded separately. No second bootstrap was launched and the guard was not relaxed to conceal that failure. A subsequent read-only inspection proved process group 16180 empty and the diagnostic cluster directory already absent. Its exact owned regular mode-0600 password file was removed after those checks; raw logs and the closed receipt remain private. The original failed scenario recovery root was untouched and its cleanup remains unconfirmed.
|
||||
|
||||
Receipt: `tests/runner-e2e/results/chat-catalog-campaign/initdb-diagnostic-receipt.json`; pre/post-capacity receipts are alongside it. These record exact command, binary/version/helper hashes, start/verification times, known PIDs, status limits, original stderr/stdout hashes and cleanup. A future diagnostic would first need a reviewed ownership-observation correction; none is authorized by this record. Another canonical initial needs meaningful environment correction or independently qualified capacity plus manager review. No arbitrary IPC deletion, kernel tuning, unrelated process stop or database access occurred.
|
||||
|
||||
Read-only resource census distinguishes the binding limit: `SEMMNI=87381` with 5,135 sets leaves 82,246 set slots, while `SEMMNS=87381` with 87,284 members leaves only 97 semaphore members. A bounded sample of 32 current-user 17-member sets all had last-member value 537 and zero waiters; 20 had every recorded member PID dead, but no campaign creator/run receipt association was established. No set was released. The other sampled sets did not satisfy all-PIDs-dead proof and are not cleanup candidates.
|
||||
|
||||
The helper's offline guard correction now retains observations and waits for its direct owned child when strict executable validation fails; it never signals an unverified group. Six offline controls pass. No second bootstrap was run. The exact executed earlier helper was reconstructed and verified against its original SHA before being retained privately; its abnormal status remains separate from the corrected helper source.
|
||||
|
||||
Source qualification found no existing external-Postgres substitute in this Product E2E lane: `server-config.ts` creates embedded-Postgres configuration; `harness-env.ts` strips and rejects ambient database URLs; `instance-isolation.ts` rejects a nonembedded mode or connection string. The product itself supports external Postgres, but using it here would require an explicit isolated-target capability and provenance review. No credential access, network request or database creation was performed for that investigation. Current capacity is still the known failed 97-member headroom; another canonical launch requires a reviewed meaningful resource/environment correction. Original sign-in cleanup and all attempt budgets remain unchanged.
|
||||
|
||||
A concrete dry-run cleanup manifest now lists exactly 20 currently user-owned PostgreSQL-marker sets with all recorded member PIDs dead and no waiters. It records IDs, owner/creator UID/GID, key/sequence, mode, change/operation timestamps and every member PID/value/waiter hash, checked twice without mutation. Campaign association is not established, so explicit user authorization is pending and **zero resources were released**. Recipe aborts on any identity/creation/state/PID reuse/permission/waiter change and rechecks each target immediately before a separately authorized removal. A bounded first eight sets would free 136 members, raising headroom from 97 to 233; this is a proposal, not a proven minimum or permission to expand. All 20 would free 340, but no removal is authorized by this document. Manifest: `results/chat-catalog-campaign/ipc-cleanup-dry-run-20.json`.
|
||||
|
||||
Privacy review supersedes the first root-inference correction in `025bf9a420`: two independently registered raw credentials can overlap as encodings, so deriving roots from their bytes was unsafe. Current correction preserves every explicitly registered raw root and carries a process-local provenance-bound expansion for replay. An overlapping-root control requires `base64(base64(A))` to remain protected when raw B equals `base64(A)`; unbound replay is rejected. The original actual report corruption/failure remains retained. No scenario or bootstrap was rerun while correcting this offline defect.
|
||||
|
||||
## Integrated finite metadata checkpoint e4b62f48aa
|
||||
|
||||
Exact archive imports verified 205 lazy definitions plus two legacy tasks: domains 43+2 legacy (41 no-gap/two partial), identity 44 (41/three), runtime 43 (43/zero), integrator 27 (26/one), external 32 (29/three), privileged 16 (16/zero). Total 207 authored/198 no-gap/nine partial/37 absent. No newer WIP is included. The canonical 244/35/191 inventory and original four-scenario/five-measurement history are preserved. Five concrete blockers and four unfinished branches remain distinct. Registered native Slack metadata is supported, but future Slack callback authoring remains outside this source checkpoint.
|
||||
|
||||
Diagnostics/reporting integration preserves isolated source refs `0fdcb539a8` and `025bf9a420` and adopts their coordinator-owned changes without modifying the executed snapshot. Current raw-root expansion uses explicit process-local provenance and preserves independently registered overlapping credentials; seven narrow controls and 400 related evidence/recovery controls passed. Six Python diagnostic guards passed. No production code was edited. No second bootstrap, canonical admission, final retry or IPC release occurred while authorization for the bounded dry-run recovery proposal remained pending.
|
||||
@@ -178,7 +178,7 @@ its standard dashboard is explicitly scoped to the latest campaign.
|
||||
|
||||
The frozen denominator is 244 requests across 35 areas and 191 capabilities. The [durable progress record](../../doc/plans/2026-10-26-chat-catalog-eval-progress.md) and `chat-campaign-manifest.json` distinguish complete, partial, unimplemented, blocked and actual measured results. Discovery and oracle calibration are not live acceptance. Each canonical scenario has one initial attempt and at most one coherent correction/rerun, with automatic retries zero; failures and unrun scenarios remain in the denominator.
|
||||
|
||||
Domain descriptors declare real setup/actions/independent grading and exact bounds. `setupHumanMessageCount` defaults to zero; genuine earlier human turns and native setup runs remain in the declared total, with persisted comment/run IDs captured before the canonical query. No historical model reply is fabricated.
|
||||
Domain descriptors declare real setup/actions/independent grading and exact bounds. `expectedHumanMessageCount` and `setupHumanMessageCount` count only the source standing-chat history; genuine human comments and operator actions elsewhere are retained separately in scenario evidence. `setupHumanMessageCount` defaults to zero; genuine earlier source-chat human turns and native setup runs remain in the declared total, with persisted comment/run IDs captured before the canonical query. No historical model reply is fabricated.
|
||||
|
||||
Only `steering.preserve-draft` and `tasks.attachment-brief` may currently declare one `queryAttachments` receipt. The canonical typed query remains unchanged; the production composer appends its exact attachment reference. Before and after send, the harness independently reads the public same-source receipt and actual bytes, verifies SHA-256 and human attribution, and binds it to the real canonical comment. Extra markdown, text changes, foreign receipts and hidden followups fail. Attachment fingerprints pin both production composer source hashes as data, without importing the UI graph.
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { expect, it } from "vitest";
|
||||
import { abnormalFinalizationResult } from "./abnormal-finalization.js";
|
||||
import type { RunnerE2EResult } from "./types.js";
|
||||
it("retains a real failed child stderr and secondary packaging error as one failed measurement", () => {
|
||||
const child = spawnSync(process.execPath, ["-e", 'process.stderr.write("owned offline bootstrap failed\\n");process.exit(1)'], { encoding: "utf8" });
|
||||
expect(child.status).toBe(1);
|
||||
const original = { schema: "paperclip.runner-e2e.result/v2", executionId: "chat-features.fixture.local.example", source: { sha: "a".repeat(40) },
|
||||
status: "failed", failureClass: "transient_infrastructure", error: child.stderr.trim(), cleanup: "not_started", attempt: 1 } as RunnerE2EResult;
|
||||
const result = abnormalFinalizationResult(original, new SyntaxError("reporter JSON parse failed"));
|
||||
expect(result).toMatchObject({ status: "failed", failureClass: "transient_infrastructure", cleanup: "not_started", attempt: 1, source: original.source });
|
||||
expect(result.error).toContain("owned offline bootstrap failed"); expect(result.error).toContain("reporter JSON parse failed");
|
||||
expect(original.error).not.toContain("reporter JSON");
|
||||
});
|
||||
it("never turns a valid cleanup receipt into successful behavior after packaging fails", () => {
|
||||
const original = { schema: "paperclip.runner-e2e.result/v2", executionId: "fixture", attempt: 1, profileId: "fixture", environmentId: "local", caseId: "fixture", provider: null, model: null, runtimeMode: "none", startedAt: new Date().toISOString(), finishedAt: new Date().toISOString(), durationMs: 0, status: "passed", cleanup: "passed" } satisfies RunnerE2EResult;
|
||||
expect(abnormalFinalizationResult(original, new Error("artifact write failed"))).toMatchObject({ status: "failed", cleanup: "passed", failureClass: "permanent_infrastructure" });
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RunnerE2EResult } from "./types.js";
|
||||
|
||||
/** Evidence failure must retain the original startup/work verdict. A generated
|
||||
* fallback remains a failed harness result, never proof that a browser ran. */
|
||||
export function abnormalFinalizationResult(original: RunnerE2EResult, error: unknown): RunnerE2EResult {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
return { ...original, status: "failed", failureClass: original.status === "failed" ? original.failureClass : "permanent_infrastructure",
|
||||
error: `${original.error ? `${original.error}; ` : ""}Abnormal attempt finalization: ${detail}` };
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"""One explicitly authorized, owned initdb bootstrap diagnostic; no server/model/case.
|
||||
Raw stderr remains private. Never stops unrelated processes or removes host IPC.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
SNAPSHOT = pathlib.Path('/private/tmp/paperclip-chat-candidate-0fdcb539a8')
|
||||
PACKAGE = 'node_modules/.pnpm/@embedded-postgres+darwin-arm64@18.1.0-beta.16/node_modules/@embedded-postgres/darwin-arm64'
|
||||
BINARY = SNAPSHOT / PACKAGE / 'native/bin/initdb'
|
||||
PREFIX = 'paperclip-chat-initdb-diagnostic-'
|
||||
|
||||
|
||||
def sha(filename):
|
||||
return hashlib.sha256(pathlib.Path(filename).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def private_json(filename, value):
|
||||
with os.fdopen(os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'w') as output:
|
||||
json.dump(value, output, indent=2)
|
||||
output.write('\n')
|
||||
|
||||
|
||||
def validate(binary, digest, root, token):
|
||||
binary, root = pathlib.Path(binary), pathlib.Path(root)
|
||||
if binary != BINARY or binary.resolve(strict=True) != BINARY or sha(binary) != digest:
|
||||
raise ValueError('Unverified embedded initdb executable')
|
||||
parent = pathlib.Path('/private/tmp').resolve()
|
||||
if root.parent != parent or not root.name.startswith(PREFIX) or root.is_symlink():
|
||||
raise ValueError('Wrong owned diagnostic root')
|
||||
info = root.lstat()
|
||||
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700:
|
||||
raise ValueError('Unsafe diagnostic root ownership')
|
||||
marker = root / 'owner.json'
|
||||
mark_info = marker.lstat()
|
||||
if marker.is_symlink() or mark_info.st_uid != os.getuid() or stat.S_IMODE(mark_info.st_mode) != 0o600:
|
||||
raise ValueError('Unsafe owner marker')
|
||||
value = json.loads(marker.read_text())
|
||||
if value != {'version': 1, 'creatorPid': os.getpid(), 'uid': os.getuid(), 'token': token}:
|
||||
raise ValueError('Diagnostic ownership receipt mismatch')
|
||||
return root / 'pgdata'
|
||||
|
||||
|
||||
def group_members(group):
|
||||
rows = []
|
||||
output = subprocess.check_output(['ps', '-axo', 'uid=,pid=,ppid=,pgid=,comm='], text=True)
|
||||
for line in output.splitlines():
|
||||
parts = line.strip().split(None, 4)
|
||||
if len(parts) == 5 and int(parts[3]) == group:
|
||||
rows.append({'uid': int(parts[0]), 'pid': int(parts[1]), 'parentPid': int(parts[2]), 'group': int(parts[3]), 'executable': parts[4]})
|
||||
return rows
|
||||
|
||||
|
||||
def verify_members(rows):
|
||||
for row in rows:
|
||||
path = pathlib.Path(row['executable'])
|
||||
if row['uid'] != os.getuid() or path.parent != BINARY.parent or path.name not in ['initdb', 'postgres']:
|
||||
raise RuntimeError('Owned diagnostic process group cannot be safely verified')
|
||||
|
||||
|
||||
def record_members(members, observed, ownership_findings):
|
||||
observed.update({row['pid']: row for row in members})
|
||||
try:
|
||||
verify_members(members)
|
||||
except RuntimeError:
|
||||
ownership_findings.append('Process-group member failed strict executable ownership guard')
|
||||
|
||||
|
||||
def cause(stderr, password):
|
||||
lines = [line.replace(password, '[REDACTED]') for line in stderr.splitlines()
|
||||
if re.search(r'FATAL:|PANIC:|ERROR:|initdb: error:|could not|No space left|Invalid argument', line, re.I)]
|
||||
return lines[:10]
|
||||
|
||||
|
||||
def run(expected_digest):
|
||||
# Actual profile credentials and the campaign oracle key are never inherited.
|
||||
original = SNAPSHOT.parent / 'paperclip-chat-candidate-e652b0de6e' / PACKAGE / 'native/bin/initdb'
|
||||
if sha(original) != expected_digest or sha(BINARY) != expected_digest:
|
||||
raise ValueError('Immutable dependency binary receipts differ')
|
||||
version = subprocess.check_output([str(BINARY), '--version'], text=True, env={'PATH': '/usr/bin:/bin', 'LC_ALL': 'C'}).strip()
|
||||
root = pathlib.Path(tempfile.mkdtemp(prefix=PREFIX, dir='/private/tmp'))
|
||||
token = secrets.token_hex(24)
|
||||
private_json(root / 'owner.json', {'version': 1, 'creatorPid': os.getpid(), 'uid': os.getuid(), 'token': token})
|
||||
data = validate(BINARY, expected_digest, root, token)
|
||||
if data.exists():
|
||||
raise ValueError('Diagnostic cluster must not exist before admission')
|
||||
password = secrets.token_urlsafe(32)
|
||||
password_file = root / 'password'
|
||||
with os.fdopen(os.open(password_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'w') as output:
|
||||
output.write(password + '\n')
|
||||
arguments = [str(BINARY), '--pgdata=' + str(data), '--auth=password', '--username=paperclip', '--pwfile=' + str(password_file), '--lc-messages=C', '--encoding=UTF8', '--locale=C', '--lc-messages=C']
|
||||
started = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
observed = {}
|
||||
timed_out = False
|
||||
ownership_findings = []
|
||||
with os.fdopen(os.open(root / 'stdout.log', os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'wb') as stdout, os.fdopen(os.open(root / 'stderr.log', os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'wb') as stderr:
|
||||
child = subprocess.Popen(arguments, stdout=stdout, stderr=stderr, start_new_session=True,
|
||||
env={'PATH': '/usr/bin:/bin', 'LC_ALL': 'C', 'LC_MESSAGES': 'C', 'HOME': str(root), 'TMPDIR': str(root)})
|
||||
print(json.dumps({"phase": "owned-bootstrap-started", "pid": child.pid, "privateRoot": str(root)}), flush=True)
|
||||
deadline = time.monotonic() + 60
|
||||
while child.poll() is None:
|
||||
members = group_members(child.pid)
|
||||
# Never lose stderr/status or signal an unverified transient member.
|
||||
record_members(members, observed, ownership_findings)
|
||||
if time.monotonic() > deadline:
|
||||
timed_out = True
|
||||
# The private session was created by this Popen. Verify all current
|
||||
# members before either signal; never signal an unverified group.
|
||||
try:
|
||||
verify_members(group_members(child.pid))
|
||||
except RuntimeError:
|
||||
ownership_findings.append('Unverified group at deadline; only direct owned child may be stopped')
|
||||
if ownership_findings:
|
||||
child.terminate()
|
||||
else:
|
||||
os.killpg(child.pid, signal.SIGTERM)
|
||||
try:
|
||||
child.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
if ownership_findings:
|
||||
child.kill()
|
||||
else:
|
||||
verify_members(group_members(child.pid))
|
||||
os.killpg(child.pid, signal.SIGKILL)
|
||||
break
|
||||
time.sleep(0.05)
|
||||
exit_code = child.wait(timeout=5)
|
||||
remaining = group_members(child.pid)
|
||||
if remaining:
|
||||
ownership_findings.append('Diagnostic process group still has participants; cluster/password retained')
|
||||
validate(BINARY, expected_digest, root, token)
|
||||
stderr_text = (root / 'stderr.log').read_text(errors='replace')
|
||||
# Only this freshly allocated marker-bound cluster is eligible for removal.
|
||||
if data.is_symlink():
|
||||
raise RuntimeError('Diagnostic pgdata became a symlink; root retained')
|
||||
if not remaining:
|
||||
if data.exists():
|
||||
shutil.rmtree(data)
|
||||
password_file.unlink()
|
||||
receipt = {'schema': 'paperclip.initdb-infrastructure-diagnostic/v1', 'infrastructureOnly': True,
|
||||
'canonicalAttempts': 0, 'providerCalls': 0, 'nativeRuns': 0, 'sourceSnapshot': str(SNAPSHOT),
|
||||
'binary': str(BINARY), 'binarySha256': expected_digest, 'binaryVersion': version,
|
||||
'helperSha256': sha(__file__), 'packageVersion': json.loads((SNAPSHOT / PACKAGE / 'package.json').read_text())['version'],
|
||||
'command': arguments, 'startedAt': started, 'finishedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'pid': child.pid, 'processGroup': child.pid, 'observedOwnedProcesses': list(observed.values()),
|
||||
'exitCode': exit_code, 'timedOut': timed_out, 'ownershipGuardFindings': sorted(set(ownership_findings)), 'stderrSha256': sha(root / 'stderr.log'),
|
||||
'stdoutSha256': sha(root / 'stdout.log'), 'sanitizedCause': cause(stderr_text, password),
|
||||
'cleanup': {'ownedProcessGroupEmpty': not remaining, 'ownedClusterDataRemoved': not data.exists(), 'passwordFileRemoved': not password_file.exists(), 'diagnosticLogsRetainedPrivately': True}}
|
||||
private_json(root / 'receipt.json', receipt)
|
||||
return {'receiptPath': str(root / 'receipt.json'), 'exitCode': exit_code, 'sanitizedCause': receipt['sanitizedCause'], 'cleanup': receipt['cleanup']}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--expected-sha256', required=True)
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(run(args.expected_sha256)))
|
||||
@@ -0,0 +1,60 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location('diagnostic', pathlib.Path(__file__).with_name('chat-campaign-initdb-diagnostic.py'))
|
||||
diag = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(diag)
|
||||
|
||||
class DiagnosticGuardTests(unittest.TestCase):
|
||||
def test_wrong_binary_rejected_before_spawn(self):
|
||||
with patch.object(diag.subprocess, 'Popen') as spawn:
|
||||
with self.assertRaisesRegex(ValueError, 'Unverified'):
|
||||
diag.validate('/bin/echo', 'not-the-pinned-binary', pathlib.Path('/private/tmp/unowned'), 'token')
|
||||
spawn.assert_not_called()
|
||||
|
||||
def test_wrong_root_rejected_even_with_matching_binary_hash(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
binary = pathlib.Path(temporary).resolve() / 'initdb'; binary.write_text('offline synthetic fixture')
|
||||
with patch.object(diag, 'BINARY', binary), patch.object(diag.subprocess, 'Popen') as spawn:
|
||||
with self.assertRaisesRegex(ValueError, 'Wrong owned'):
|
||||
diag.validate(binary, diag.sha(binary), pathlib.Path(temporary), 'token')
|
||||
spawn.assert_not_called()
|
||||
|
||||
def test_wrong_owner_receipt_and_symlink_fail_closed(self):
|
||||
with tempfile.TemporaryDirectory(prefix=diag.PREFIX, dir='/private/tmp') as temporary:
|
||||
root = pathlib.Path(temporary); binary = root / 'initdb'; binary.write_text('offline synthetic fixture')
|
||||
diag.private_json(root / 'owner.json', {'version': 1, 'creatorPid': os.getpid(), 'uid': os.getuid(), 'token': 'correct'})
|
||||
with patch.object(diag, 'BINARY', binary):
|
||||
with self.assertRaisesRegex(ValueError, 'receipt mismatch'):
|
||||
diag.validate(binary, diag.sha(binary), root, 'wrong')
|
||||
(root / 'owner.json').chmod(0o644)
|
||||
with self.assertRaisesRegex(ValueError, 'Unsafe owner'):
|
||||
diag.validate(binary, diag.sha(binary), root, 'correct')
|
||||
|
||||
def test_only_sanitized_cause_is_public(self):
|
||||
secret = 'synthetic-private-password'
|
||||
result = diag.cause('FATAL: could not start ' + secret + '\nordinary raw details\n', secret)
|
||||
self.assertNotIn(secret, json.dumps(result))
|
||||
self.assertIn('[REDACTED]', result[0])
|
||||
self.assertNotIn('ordinary raw details', json.dumps(result))
|
||||
|
||||
def test_unowned_group_cannot_be_signaled(self):
|
||||
with self.assertRaisesRegex(RuntimeError, 'safely verified'):
|
||||
diag.verify_members([{'uid': os.getuid(), 'executable': '/bin/unrelated'}])
|
||||
|
||||
def test_secondary_ownership_guard_retains_observation_without_throwing_away_capture(self):
|
||||
observed, findings = {}, []
|
||||
row = {'pid': 123, 'uid': os.getuid(), 'executable': '/bin/transient-unverified'}
|
||||
with patch.object(diag.os, 'killpg') as signal_group:
|
||||
diag.record_members([row], observed, findings)
|
||||
self.assertEqual(observed[123], row)
|
||||
self.assertTrue(findings)
|
||||
signal_group.assert_not_called()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,8 @@
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { campaignSummary, completeCampaignAttempt, freezeChatCampaign, reserveCampaignAttempt, validateFrozenCampaign, type CampaignAttempt, campaignProductIdentityEligible, type CampaignPublishedIdentity } from "./chat-campaign.js";
|
||||
import { campaignSummary, completeCampaignAttempt, freezeChatCampaign, reserveCampaignAttempt, validateFrozenCampaign, type CampaignAttempt, campaignProductIdentityEligible, type CampaignPublishedIdentity, campaignAttemptProcessed } from "./chat-campaign.js";
|
||||
const root = new URL("../../", import.meta.url).pathname;
|
||||
const base = "8373d7867963674ff960d2345d0d28086f799bc7";
|
||||
function attempt(ordinal: 1 | 2, executionId: string): CampaignAttempt {
|
||||
@@ -122,3 +125,54 @@ it("does not complete or report a qualifying pass when its product identity is m
|
||||
Object.assign(reserved, completion);
|
||||
expect(campaignSummary(campaign)).toMatchObject({ initialPassed: 0, finalPassed: 0, productEvidence: { identityIneligibleMeasurements: 1, measurementsByIdentity: { unknown: 1 } } });
|
||||
});
|
||||
|
||||
it("processes concrete unsupported probes without accepting incomplete outcomes or unfinished shells", () => {
|
||||
const campaign = freezeChatCampaign(root, base), scenario = campaign.scenarios.find(s => s.id === "tasks.create-backlog")!;
|
||||
scenario.readiness = "partial"; scenario.authoringStatus = "concrete-blocker";
|
||||
scenario.blockerProbeSources = ["chat-catalog-domains/identity-access-grants.ts"];
|
||||
const a = attempt(1, scenario.executionId!); reserveCampaignAttempt(scenario, a);
|
||||
const completion = { outcome: "passed" as const, finishedAt: new Date().toISOString(), definitionDigest: "fixture", graderDigest: "fixture", evidencePaths: ["tests/runner-e2e/results/fixture/result.json"], cleanup: "passed" as const, usage: a.usage, classification: null, observedFailure: null, corePrDependencies: [] };
|
||||
expect(() => completeCampaignAttempt(scenario, completion)).toThrow("Incomplete conditions");
|
||||
completeCampaignAttempt(scenario, { ...completion, outcome: "blocked" });
|
||||
expect(campaignSummary(campaign)).toMatchObject({ authored: 2, coverageComplete: 1, concreteBlockers: 1, unfinished: 0, authoringResolved: 2, processed: 0, finalPassed: 0, mergeEligible: false });
|
||||
scenario.authoringStatus = "unfinished"; scenario.attempts = [];
|
||||
expect(() => reserveCampaignAttempt(scenario, attempt(1, scenario.executionId!))).toThrow("no executable implementation");
|
||||
expect(campaignSummary(campaign)).toMatchObject({ unfinished: 1, authoringResolved: 1 });
|
||||
});
|
||||
it("requires a real terminal source-attributed result for processing, not a reservation or calibration receipt", () => {
|
||||
const temporary = mkdtempSync(path.join(tmpdir(), "chat-campaign-processing-"));
|
||||
try {
|
||||
const a = attempt(1, "chat-features.runner-codex-subscription.local.fixture");
|
||||
a.outcome = "blocked"; a.finishedAt = new Date().toISOString();
|
||||
const relative = "tests/runner-e2e/results/owned/result.json", filename = path.join(temporary, relative);
|
||||
a.evidencePaths = [relative]; mkdirSync(path.dirname(filename), { recursive: true });
|
||||
expect(campaignAttemptProcessed(a, temporary)).toBe(false);
|
||||
const raw = { executionId: a.executionId, source: { sha: a.sourceRevision }, status: "failed", startedAt: a.startedAt, finishedAt: a.finishedAt };
|
||||
writeFileSync(filename, JSON.stringify(raw)); expect(campaignAttemptProcessed(a, temporary)).toBe(true);
|
||||
writeFileSync(filename, JSON.stringify({ ...raw, source: { sha: "wrong" } })); expect(campaignAttemptProcessed(a, temporary)).toBe(false);
|
||||
writeFileSync(filename, JSON.stringify({ calibration: true })); expect(campaignAttemptProcessed(a, temporary)).toBe(false);
|
||||
} finally { rmSync(temporary, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("permits a processed unsupported remainder while requiring every unfinished branch to close", () => {
|
||||
const temporary = mkdtempSync(path.join(tmpdir(), "chat-campaign-gate-"));
|
||||
try {
|
||||
const campaign = freezeChatCampaign(root, base); let passes = 0;
|
||||
for (const [index, s] of campaign.scenarios.entries()) {
|
||||
s.readiness = "implemented"; s.authoringStatus = "complete"; s.executionId = `chat-features.runner-codex-subscription.local.${s.id}`;
|
||||
s.qualifyingDefinitionDigest = "fixture"; s.qualifyingGraderDigest = "fixture";
|
||||
const a = attempt(1, s.executionId); a.finishedAt = a.startedAt; a.cleanup = "passed";
|
||||
a.outcome = s.productPolicy?.lane === "source-build" && passes < 196 ? "passed" : "blocked";
|
||||
if (a.outcome === "passed") passes++;
|
||||
const relative = `tests/runner-e2e/results/gate-${index}/result.json`, filename = path.join(temporary, relative);
|
||||
mkdirSync(path.dirname(filename), { recursive: true });
|
||||
writeFileSync(filename, JSON.stringify({ executionId: a.executionId, source: { sha: a.sourceRevision }, status: a.outcome === "passed" ? "passed" : "failed", startedAt: a.startedAt, finishedAt: a.finishedAt }));
|
||||
a.evidencePaths = [relative]; s.attempts.push(a);
|
||||
}
|
||||
const blocked = campaign.scenarios.find(s => s.attempts[0]!.outcome === "blocked")!;
|
||||
blocked.readiness = "partial"; blocked.authoringStatus = "concrete-blocker"; blocked.blockerProbeSources = ["actual-supported-surface-probe.ts"];
|
||||
expect(campaignSummary(campaign, temporary)).toMatchObject({ authoringResolved: 244, processed: 244, finalPassed: 196, mergeEligible: true });
|
||||
blocked.authoringStatus = "unfinished";
|
||||
expect(campaignSummary(campaign, temporary)).toMatchObject({ authoringResolved: 243, unfinished: 1, mergeEligible: false });
|
||||
} finally { rmSync(temporary, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -87,6 +87,9 @@ export interface CampaignScenario {
|
||||
qualifyingGraderDigest: string | null;
|
||||
owner: string;
|
||||
readiness: "unimplemented" | "partial" | "implemented" | "blocked";
|
||||
/** Complete callbacks, executable supported-surface blocker, unfinished conditional, or absent. */
|
||||
authoringStatus?: "complete" | "concrete-blocker" | "unfinished" | "absent";
|
||||
blockerProbeSources?: string[];
|
||||
executionId: string | null;
|
||||
blocker: string | null;
|
||||
productPolicy?: { lane: CampaignProductLane; sourceBaselineRevision: string };
|
||||
@@ -147,7 +150,7 @@ export function validateFrozenCampaign(campaign: ChatCampaign, root: string): vo
|
||||
}
|
||||
export function reserveCampaignAttempt(scenario: CampaignScenario, attempt: CampaignAttempt): void {
|
||||
if (!scenario.productPolicy || !identityRevision(scenario.productPolicy.sourceBaselineRevision) || scenario.productPolicy.lane !== scenarioProductPolicy(scenario.id, scenario.productPolicy.sourceBaselineRevision).lane) throw new Error("Attempt requires a declared product lane");
|
||||
if (scenario.readiness !== "implemented" || !scenario.executionId) throw new Error("Scenario has no executable implementation");
|
||||
if (!(scenario.readiness === "implemented" || (scenario.readiness === "partial" && scenario.authoringStatus === "concrete-blocker" && scenario.blockerProbeSources?.length)) || !scenario.executionId) throw new Error("Scenario has no executable implementation");
|
||||
if (scenario.attempts.some(entry => entry.outcome === "running")) throw new Error("Previous attempt still running");
|
||||
if (scenario.attempts.length >= 2 || attempt.ordinal !== scenario.attempts.length + 1) throw new Error("Attempt budget exhausted or incorrect ordinal");
|
||||
if (scenario.attempts[0]?.outcome === "passed") throw new Error("Passing scenario cannot be rerun");
|
||||
@@ -166,24 +169,44 @@ export function completeCampaignAttempt(scenario: CampaignScenario, completion:
|
||||
if (!attempt || attempt.outcome !== "running") throw new Error("No running reserved attempt");
|
||||
if (completion.definitionDigest !== attempt.definitionDigest || completion.graderDigest !== attempt.graderDigest) throw new Error("Definition or grader changed during attempt");
|
||||
if (completion.evidencePaths.some(value => !value.startsWith("tests/runner-e2e/results/") || value.split("/").includes(".."))) throw new Error("Evidence must use retained sanitized result paths");
|
||||
if (completion.outcome === "passed" && (scenario.readiness !== "implemented" || (scenario.authoringStatus && scenario.authoringStatus !== "complete"))) throw new Error("Incomplete conditions cannot pass");
|
||||
if (completion.outcome === "passed" && (!completion.evidencePaths.length || completion.cleanup !== "passed")) throw new Error("Pass requires evidence and successful cleanup");
|
||||
if (completion.outcome === "passed" && !campaignProductIdentityEligible(scenario, { ...attempt, ...completion })) throw new Error("Pass requires eligible product identity");
|
||||
if (Date.parse(completion.finishedAt) < Date.parse(attempt.startedAt) || !Number.isFinite(Date.parse(completion.finishedAt))) throw new Error("Invalid completion timing");
|
||||
Object.assign(attempt, completion);
|
||||
}
|
||||
export function campaignSummary(campaign: ChatCampaign) {
|
||||
const initialPassed = campaign.scenarios.filter(scenario => { const first = scenario.attempts[0]; return first?.outcome === "passed" && first.cleanup === "passed" && first.corePrDependencies.length === 0 && campaignProductIdentityEligible(scenario, first, campaign.baselineRevision); }).length;
|
||||
const passed = campaign.scenarios.filter(scenario => { const final = scenario.attempts.at(-1); return final?.outcome === "passed" && final.corePrDependencies.length === 0 && final.cleanup === "passed" && final.definitionDigest === scenario.qualifyingDefinitionDigest && final.graderDigest === scenario.qualifyingGraderDigest && campaignProductIdentityEligible(scenario, final, campaign.baselineRevision); }).length;
|
||||
/** A reservation or offline calibration alone never establishes actual processing. */
|
||||
export function campaignAttemptProcessed(attempt: CampaignAttempt, root = process.cwd()): boolean {
|
||||
if (attempt.outcome === "running" || !attempt.finishedAt) return false;
|
||||
return attempt.evidencePaths.some(relative => {
|
||||
if (!relative.startsWith("tests/runner-e2e/results/") || relative.split("/").includes("..") || path.basename(relative) !== "result.json") return false;
|
||||
try {
|
||||
const result = JSON.parse(readFileSync(path.join(root, relative), "utf8"));
|
||||
return result.executionId === attempt.executionId && result.source?.sha === attempt.sourceRevision
|
||||
&& ["passed", "failed"].includes(result.status) && (attempt.outcome !== "passed" || result.status === "passed")
|
||||
&& Number.isFinite(Date.parse(result.startedAt)) && Number.isFinite(Date.parse(result.finishedAt))
|
||||
&& Date.parse(result.finishedAt) >= Date.parse(result.startedAt);
|
||||
} catch { return false; }
|
||||
});
|
||||
}
|
||||
export function campaignSummary(campaign: ChatCampaign, root = process.cwd()) {
|
||||
const initialPassed = campaign.scenarios.filter(scenario => { const first = scenario.attempts[0]; return scenario.readiness === "implemented" && (!scenario.authoringStatus || scenario.authoringStatus === "complete") && first?.outcome === "passed" && first.cleanup === "passed" && first.corePrDependencies.length === 0 && campaignProductIdentityEligible(scenario, first, campaign.baselineRevision); }).length;
|
||||
const passed = campaign.scenarios.filter(scenario => { const final = scenario.attempts.at(-1); return scenario.readiness === "implemented" && (!scenario.authoringStatus || scenario.authoringStatus === "complete") && final?.outcome === "passed" && final.corePrDependencies.length === 0 && final.cleanup === "passed" && final.definitionDigest === scenario.qualifyingDefinitionDigest && final.graderDigest === scenario.qualifyingGraderDigest && campaignProductIdentityEligible(scenario, final, campaign.baselineRevision); }).length;
|
||||
const attempted = campaign.scenarios.filter(scenario => scenario.attempts.length).length;
|
||||
const implemented = campaign.scenarios.filter(scenario => scenario.readiness === "implemented").length;
|
||||
const implemented = campaign.scenarios.filter(scenario => scenario.readiness === "implemented" && (!scenario.authoringStatus || scenario.authoringStatus === "complete")).length;
|
||||
const concreteBlockers = campaign.scenarios.filter(s => s.authoringStatus === "concrete-blocker" && s.executionId && s.blockerProbeSources?.length).length;
|
||||
const authored = campaign.scenarios.filter(s => s.readiness !== "unimplemented" && s.executionId).length;
|
||||
const unfinished = authored - implemented - concreteBlockers;
|
||||
const authoringResolved = implemented + concreteBlockers;
|
||||
const processed = campaign.scenarios.filter(s => s.attempts.some(a => campaignAttemptProcessed(a, root))).length;
|
||||
const measured = campaign.scenarios.flatMap(scenario => scenario.attempts.map(attempt => ({ scenario, attempt })));
|
||||
const productEvidence = { sourceBaselineRevision: campaign.baselineRevision,
|
||||
measurementsByIdentity: Object.fromEntries(["source-build", "published-release", "source-built-installed", "unknown"].map(kind => [kind, measured.filter(row => (row.attempt.productIdentity?.kind ?? "unknown") === kind).length])),
|
||||
identityIneligibleMeasurements: measured.filter(row => !campaignProductIdentityEligible(row.scenario, row.attempt, campaign.baselineRevision)).length };
|
||||
return { productEvidence, denominator: campaign.denominator, implemented, attempted, unrun: campaign.denominator - attempted,
|
||||
return { authored, coverageComplete: implemented, concreteBlockers, unfinished, authoringResolved, processed, sourceChatHumanCountScope: "standing-source-chat-only", productEvidence, denominator: campaign.denominator, implemented, attempted, unrun: campaign.denominator - attempted,
|
||||
initialPassed, finalPassed: passed, firstAttemptRate: initialPassed / campaign.denominator,
|
||||
finalRate: passed / campaign.denominator, requiredPasses: campaign.requiredPasses,
|
||||
mergeEligible: implemented === campaign.denominator && attempted === campaign.denominator && passed >= campaign.requiredPasses && !campaign.scenarios.some(scenario => scenario.attempts.some(attempt => attempt.outcome === "running")) };
|
||||
mergeEligible: authoringResolved === campaign.denominator && processed === campaign.denominator && passed >= campaign.requiredPasses && !campaign.scenarios.some(scenario => scenario.attempts.some(attempt => attempt.outcome === "running" || attempt.cleanup === "failed" || attempt.cleanup === "pending")) };
|
||||
}
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
const [command, baseline] = process.argv.slice(2);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,8 @@
|
||||
import { abnormalFinalizationResult } from "./abnormal-finalization.js";
|
||||
import { CHAT_ORACLE_KEY_ENV } from "./chat-private-oracle.js";
|
||||
import { stagePublishedChatInstall, ChatInstalledCleanupError } from "./chat-installed-lane.js";
|
||||
import { stageChatPrivateInputs, chatLaunchCredentialNames, CHAT_PRIVATE_INPUTS_ENV } from "./chat-private-inputs.js";
|
||||
import { createPrivateDiagnosticStream, diagnosticSecrets, PRIVATE_DIAGNOSTICS_ENV, readPrivateDiagnosticSecrets, stagePrivateDiagnostics } from "./private-diagnostics.js";
|
||||
import { createPrivateDiagnosticStream, PRIVATE_DIAGNOSTICS_ENV, readPrivateDiagnosticSecrets, stagePrivateDiagnostics } from "./private-diagnostics.js";
|
||||
import { sanitizePrivateReporterArtifacts } from "./private-reporter-artifacts.js";
|
||||
import { findChatCatalogCase } from "./chat-catalog-registry.js";
|
||||
import { recoverWorkerTiming } from "./worker-timing.js";
|
||||
@@ -648,7 +649,7 @@ async function runAttempt(input: {
|
||||
options.ui || options.debug,
|
||||
() => readPrivateDiagnosticSecrets(diagnosticRegistry, temporaryRoot),
|
||||
);
|
||||
credentials.splice(0, credentials.length, ...diagnosticSecrets(readPrivateDiagnosticSecrets(diagnosticRegistry, temporaryRoot)));
|
||||
credentials.splice(0, credentials.length, ...readPrivateDiagnosticSecrets(diagnosticRegistry, temporaryRoot));
|
||||
processCleanupFailed = processResult.processCleanupError !== null;
|
||||
if (options.installedStartupOnly) {
|
||||
const proofDir = path.join(resultsRoot, campaignId, "installed-startup-only");
|
||||
@@ -900,6 +901,30 @@ async function runAttempt(input: {
|
||||
);
|
||||
}
|
||||
return [...publishedResults];
|
||||
} catch (error) {
|
||||
// Packaging/diagnostic failures must not erase an actually launched attempt
|
||||
// or its primary startup failure. Retain only closed sanitized metadata;
|
||||
// raw reports stay private when their normal packaging path failed.
|
||||
for (const candidate of executions) {
|
||||
if (publishedResultPaths.has(candidate.id)) continue;
|
||||
const casePrivateDir = path.join(temporaryRoot, "artifacts-private", "cases", candidate.task.id);
|
||||
const fallback = syntheticResult(candidate, attempt, startedAtMs, "Attempt terminated before a worker result was retained");
|
||||
const original = await readResult(path.join(casePrivateDir, "result.json"), fallback);
|
||||
const result = abnormalFinalizationResult(original, error);
|
||||
const uploadDir = path.join(resultsRoot, campaignId, candidate.suite.id, candidate.profile.id, candidate.environment.id, candidate.task.id, `attempt-${attempt}`);
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
const resultPath = path.join(uploadDir, "result.json");
|
||||
const safe = `${JSON.stringify(sanitizeJson(result, attemptSecrets), null, 2)}\n`;
|
||||
assertSecretFree(safe, attemptSecrets, resultPath);
|
||||
await writeFile(resultPath, safe, "utf8");
|
||||
await writeFile(path.join(uploadDir, "abnormal-finalization.json"), `${JSON.stringify(sanitizeJson({
|
||||
schemaVersion: 1, executionId: candidate.id, originalResult: original,
|
||||
packaging: "failed; raw diagnostics retained privately", error: error instanceof Error ? error.message : String(error),
|
||||
}, attemptSecrets), null, 2)}\n`, { mode: 0o600 });
|
||||
if (rawCleanupResults.length < executions.length) rawCleanupResults.push({ cleanup: original.cleanup, synthetic: original === fallback, status: original.status });
|
||||
publishedResults.push(result); publishedResultPaths.set(candidate.id, resultPath);
|
||||
}
|
||||
return [...publishedResults];
|
||||
} finally {
|
||||
let gatewayCleanupError: unknown;
|
||||
try { await gateway?.close(); } catch (error) { gatewayCleanupError = error; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sanitizePrivateReporterArtifacts } from "./private-reporter-artifacts.js";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, chmodSync, realpathSync } from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, chmodSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { createPrivateDiagnosticStream, diagnosticSecrets, PRIVATE_DIAGNOSTICS_ENV, readPrivateDiagnosticSecrets, stagePrivateDiagnostics, trackedPrivateSecrets } from "./private-diagnostics.js";
|
||||
@@ -13,7 +14,7 @@ describe("private diagnostic output boundary", () => {
|
||||
const outputs: string[] = []; const stream = createPrivateDiagnosticStream(() => [password, pem, short], value => outputs.push(value));
|
||||
const input = `HTTP503: π provider unavailable ${password}\n${pem}\n${short}\nJSON ${JSON.stringify(password)}\nHTML ${password.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """)}\nURI ${encodeURIComponent(password)}\nBASE64 ${Buffer.from(password).toString("base64")}\n`;
|
||||
const bytes = Buffer.from(input); for (let i = 0; i < bytes.length; i += 3) stream.write(bytes.subarray(i, i + 3)); stream.end();
|
||||
const safe = outputs.join(""); for (const value of diagnosticSecrets([password, pem, short])) expect(safe).not.toContain(value);
|
||||
const safe = outputs.join(""); for (const value of diagnosticSecrets([password, pem, short]).values) expect(safe).not.toContain(value);
|
||||
expect(safe).toContain("HTTP503: π provider unavailable"); expect(stream.redactedSecret()).toBe(true); expect(stream.failed()).toBe(false);
|
||||
});
|
||||
it("synchronously registers a generated cookie before first output and rereads at final flush", () => {
|
||||
@@ -36,3 +37,38 @@ describe("private diagnostic output boundary", () => {
|
||||
expect(stream.failed()).toBe(true); expect(outputs.join("")).not.toContain("sensitive"); expect(outputs.join("")).not.toContain("late private bytes"); expect(outputs.join("")).toContain("size bound exceeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("replays synthetic auth expansion through stream and reporter twice without inventing structural secrets", async () => {
|
||||
const auth = JSON.stringify({ tokens: { access_token: "synthetic-access-private-837362", refresh_token: "synthetic-refresh-private-937363" }, last_refresh: "2026-10-26T00:00:00Z" }, null, 2);
|
||||
const short = "Ax4\nBy5\nCz6", raw = [auth, short], once = diagnosticSecrets(raw);
|
||||
expect(diagnosticSecrets(once)).toEqual(once);
|
||||
expect(diagnosticSecrets(diagnosticSecrets(once))).toEqual(once);
|
||||
expect(once.values).not.toContain("{"); expect(once.values).not.toContain("}");
|
||||
for (const fragment of short.split("\n")) expect(once.values).toContain(fragment);
|
||||
const { root } = registry(); const outputs: string[] = [];
|
||||
const stream = createPrivateDiagnosticStream(() => raw, value => outputs.push(value));
|
||||
stream.write(Buffer.from(`initdb bootstrap exited1\n${auth}\n${short}\n`)); stream.end();
|
||||
writeFileSync(path.join(root, "server.log"), outputs.join(""));
|
||||
writeFileSync(path.join(root, "playwright-output.json"), JSON.stringify({ status: "failed" }));
|
||||
mkdirSync(path.join(root, "playwright-output"));
|
||||
const report = path.join(root, "playwright-output", ".last-run.json");
|
||||
writeFileSync(report, JSON.stringify({ status: "failed", failedTests: [], nested: { detail: JSON.parse(auth).tokens.access_token } }, null, 2));
|
||||
await sanitizePrivateReporterArtifacts(root, once); await sanitizePrivateReporterArtifacts(root, once);
|
||||
const safe = readFileSync(report, "utf8"); expect(JSON.parse(safe)).toMatchObject({ status: "failed", failedTests: [] });
|
||||
expect(safe).not.toContain(JSON.parse(auth).tokens.access_token);
|
||||
expect(readFileSync(path.join(root, "server.log"), "utf8")).toContain("initdb bootstrap exited1");
|
||||
});
|
||||
|
||||
it("preserves independently registered overlapping roots and rejects unbound expansion replay", async () => {
|
||||
const first = "synthetic-independent-root-A-873628";
|
||||
const second = Buffer.from(first).toString("base64");
|
||||
const secondEncoded = Buffer.from(second).toString("base64");
|
||||
const expanded = diagnosticSecrets([first, second, "Ax4\nBy5"]);
|
||||
expect(expanded.values).toContain(secondEncoded);
|
||||
expect(diagnosticSecrets(expanded)).toBe(expanded);
|
||||
expect(expanded.values).toContain("Ax4"); expect(expanded.values).toContain("By5");
|
||||
expect(() => diagnosticSecrets({ kind: "expanded-private-diagnostic-secrets", values: expanded.values })).toThrow("provenance");
|
||||
const outputs: string[] = []; const stream = createPrivateDiagnosticStream(() => [first, second], value => outputs.push(value));
|
||||
stream.write(Buffer.from(`independent credential ${secondEncoded}\n`)); stream.end();
|
||||
expect(outputs.join("")).not.toContain(secondEncoded);
|
||||
});
|
||||
@@ -45,25 +45,38 @@ export function trackedPrivateSecrets(initial: string[], executionId: string, en
|
||||
return Reflect.set(target, property, value);
|
||||
} });
|
||||
}
|
||||
export function diagnosticSecrets(values: readonly string[]): string[] {
|
||||
const variants = normalizedSecrets(values).flatMap(value => {
|
||||
export interface DiagnosticSecretExpansion {
|
||||
readonly kind: "expanded-private-diagnostic-secrets";
|
||||
readonly values: readonly string[];
|
||||
}
|
||||
// Provenance is process-local. Never infer raw-root status from credential bytes:
|
||||
// independently registered roots can legitimately equal another root's encoding.
|
||||
const rawDiagnosticRoots = new WeakMap<DiagnosticSecretExpansion, readonly string[]>();
|
||||
export function diagnosticSecrets(input: readonly string[] | DiagnosticSecretExpansion): DiagnosticSecretExpansion {
|
||||
if (!Array.isArray(input)) {
|
||||
const expanded = input as DiagnosticSecretExpansion;
|
||||
if (!rawDiagnosticRoots.has(expanded)) throw new Error("Diagnostic expansion has no raw-root provenance");
|
||||
return expanded;
|
||||
}
|
||||
const roots = normalizedSecrets(input);
|
||||
const variants = roots.flatMap(value => {
|
||||
let structured = false; try { const parsed = JSON.parse(value); structured = parsed !== null && typeof parsed === "object"; } catch { /* Ordinary secret. */ }
|
||||
return [value, JSON.stringify(value).slice(1, -1), encodeURIComponent(value), Buffer.from(value).toString("base64"), Buffer.from(value).toString("base64url"),
|
||||
value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"),
|
||||
// Short nonempty password/key lines are private too. JSON punctuation is
|
||||
// structural; its actual string credential values are separately indexed
|
||||
// by normalizedSecrets, and longer serialized field lines remain covered.
|
||||
...value.split(/\r?\n/).filter(line => structured ? line.length >= 8 : line.length > 0)];
|
||||
value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"),
|
||||
...value.split(/\r?\n/).filter(line => structured ? line.length >= 8 : line.length > 0)];
|
||||
});
|
||||
return normalizedSecrets(variants);
|
||||
const expanded: DiagnosticSecretExpansion = Object.freeze({ kind: "expanded-private-diagnostic-secrets", values: Object.freeze(normalizedSecrets(variants)) });
|
||||
rawDiagnosticRoots.set(expanded, Object.freeze(roots));
|
||||
return expanded;
|
||||
}
|
||||
|
||||
/** No raw byte reaches a console or log. UTF-8 and secret boundaries may span
|
||||
* chunks. Oversized unterminated lines are withheld whole, with a truthful
|
||||
* omission marker; arbitrary memory growth never forces an unsafe flush. */
|
||||
export function createPrivateDiagnosticStream(secrets: () => readonly string[], emit: (safe: string) => void, maximumLine = 1024 * 1024) {
|
||||
const decoder = new StringDecoder("utf8"); let pending = ""; let suppressing = false; let failed = false; let ended = false; let leaked = false;
|
||||
const line = (value: string) => {
|
||||
try { const values = diagnosticSecrets(secrets()); leaked ||= !!findSecretLeak(value, values); emit(redactText(value, values)); }
|
||||
try { const values = diagnosticSecrets(secrets()).values; leaked ||= !!findSecretLeak(value, values); emit(redactText(value, values)); }
|
||||
catch { failed = true; emit("[diagnostics withheld: private redaction unavailable]\n"); }
|
||||
};
|
||||
const consume = (text: string) => {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { lstat, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { inspectEmbeddedReport, inspectZip } from "./evidence.js";
|
||||
import { diagnosticSecrets } from "./private-diagnostics.js";
|
||||
import { findSecretLeak, redactText } from "./redaction.js";
|
||||
import { diagnosticSecrets, type DiagnosticSecretExpansion } from "./private-diagnostics.js";
|
||||
import { findSecretLeak, redactText, sanitizeJson } from "./redaction.js";
|
||||
export interface ReporterPrivacyIssue { file: string; disposition: "redacted" | "omitted"; reason: string }
|
||||
const sources = ["server.log", "playwright.log", "junit.xml", "html-report", "blob-report", "playwright-output"];
|
||||
const text = new Set([".log", ".txt", ".md", ".xml", ".html", ".json", ".js", ".css"]);
|
||||
/** Only reporter/diagnostic sinks are touched, after independent product oracles
|
||||
* finish. Product API snapshots and work-product bytes are never pre-sanitized.
|
||||
* Uninspectable/leaking compressed reports are omitted rather than fabricated. */
|
||||
export async function sanitizePrivateReporterArtifacts(root: string, values: readonly string[]): Promise<ReporterPrivacyIssue[]> {
|
||||
const secrets = diagnosticSecrets(values); const issues: ReporterPrivacyIssue[] = [];
|
||||
export async function sanitizePrivateReporterArtifacts(root: string, values: readonly string[] | DiagnosticSecretExpansion): Promise<ReporterPrivacyIssue[]> {
|
||||
const secrets = diagnosticSecrets(values).values; const issues: ReporterPrivacyIssue[] = [];
|
||||
const visit = async (relative: string): Promise<void> => {
|
||||
const filename = path.join(root, relative); const info = await lstat(filename).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return null; throw error; });
|
||||
if (!info) return;
|
||||
@@ -29,7 +29,11 @@ export async function sanitizePrivateReporterArtifacts(root: string, values: rea
|
||||
const opaque = extension === ".html" ? await inspectEmbeddedReport(raw, secrets) : null;
|
||||
if (opaque) { await rm(filename); issues.push({ file: safeName, disposition: "omitted", reason: opaque }); return; }
|
||||
const leak = findSecretLeak(raw, secrets);
|
||||
const safe = redactText(raw, secrets);
|
||||
let safe: string;
|
||||
if (extension === ".json") {
|
||||
try { safe = `${JSON.stringify(sanitizeJson(JSON.parse(raw), secrets), null, 2)}\n`; }
|
||||
catch { await rm(filename); issues.push({ file: safeName, disposition: "omitted", reason: "reporter JSON could not be safely parsed" }); return; }
|
||||
} else safe = redactText(raw, secrets);
|
||||
if (safe !== raw) await writeFile(filename, safe, { mode: 0o600 });
|
||||
if (leak) issues.push({ file: safeName, disposition: "redacted", reason: leak });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user