74 KiB
P1 Descriptor Evidence Configuration Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Implement P1/D1 so a schema-v3 workspace can declare a complete, non-secret Evidence source and policy, bind any source credentials from installation-local files, preserve descriptor/source identity at one Git revision, and render a harness configuration that passes tht config check.
Architecture: The canonical descriptor stays in workspaces/<id>.yaml inside the shared registry repository. Filesystem Evidence is named by a lexical repo-relative URI under workspace-content/<id>/evidence; the registry verifies that the tree and descriptor exist in the same commit, while P6—not P1—will materialize and realpath-check that tree. The backend derives source-specific installation bindings, immutable revision identity, evidence.sources, and vector policy into the same runtime YAML already used by sessions; the harness parses file-backed HTTP/S3 credentials without acquiring content. P1 is proven first by a clean-state, real-Git/real-HTTP automated process and then by an independent manual artifact walkthrough.
Tech Stack: TypeScript 5, Zod 4, Fastify 5, Git CLI, YAML, React 18, Python 3.12, Pydantic 2, pytest, Vitest, Node.js 22, Bash.
Source PRD: docs/prd/2026-08-09-workspace-preprocessing-prd.md v0.5 (208b299), especially RF1, RNF1–RNF8, D1, D6, D8, D9, P1, and “Standard di verifica obbligatorio del futuro piano P1”.
P1 completion contract
P1 implements D1 and the P1-owned prerequisites of RF1; it does not claim all of RF1 complete. In particular, P2 owns the backend-independent host renderer/preprocessing consumer needed to finish RF1.3, and P10 owns operational ssh_tunnel support for RF1.4.
P1 is complete only when all of the following are true:
- Schema v3 accepts an optional strict
evidencesection. Absence remains operational for existing workspaces (RNF3); P2 will warn when preprocessing is requested without Evidence. filesystem,http, ands3are supported descriptor source types. The descriptor contains source identity and non-secret behavior only.- Filesystem URI is exactly the workspace Evidence root,
workspace-content/<workspace-id>/evidence, using normalized POSIX segments. It is checked lexically in schema validation and checked as a Git tree at the same immutable commit during publication/activation. - HTTP query-bearing signed URLs and S3 static credentials enter only through installation-local
*_FILEbindings. Public descriptor HTTP URIs never contain userinfo, query, or fragment. No secret file content is emitted into Git, registry docs, exports, API errors, reports, or rendered YAML. - The existing backend production renderer emits a harness-compatible
evidence.sourcesentry plusvector.max_chunk_charsandvector.retain_published_generations, under the sameruntime_identity.workspace_revisionused by sessions. P1 proves that backend-runtime half of RF1.3; P2 must provide and prove a backend-independent host-CLI render path before claiming preprocessing uses the same effective config. - Existing frontend workspace flows parse, preserve, conflict-resolve, and re-publish Evidence. P1 adds only a read-only summary, not a new preprocessing or Evidence-authoring UI.
- The automated gate passes from clean state through local Git → registry → real HTTP → snapshot/docs/export → production render → real
tht config check, with deterministic outputs, negative cases, secret scanning, inspectable reports, and ownership-confined cleanup. - The manual gate uses entirely new state and remains
PENDINGuntil a human reviewer records approval.
Canonical descriptor contract
Use this exact filesystem form in the generic examples and acceptance fixture:
evidence:
source:
type: filesystem
uri: workspace-content/example/evidence
patterns:
- "**/*.md"
max_bytes: 10485760
policy:
max_chunk_chars: 4000
retain_published_generations: 3
HTTP is an explicit, non-secret manifest. authentication: signed_urls_file requires one installation file containing a JSON array of signed transport URLs in the same order as uris; the harness must verify that stripping each transport URL's query produces the declared URI before it accepts the config.
evidence:
source:
type: http
uris:
- https://evidence.example.test/guide.md
authentication: signed_urls_file # or none
connect_timeout_ms: 5000
read_timeout_ms: 30000
max_bytes: 10485760
max_redirects: 5
allow_private_hosts: false
max_cache_bytes: 67108864
policy:
max_chunk_chars: 4000
retain_published_generations: 3
S3 uses one canonical s3:// URI. credentials: static_files requires access-key and secret-key files and permits an optional session-token file; ambient delegates to the installation's AWS-compatible provider chain.
evidence:
source:
type: s3
uri: s3://evidence-bucket/example/
region: eu-west-1
credentials: static_files # or ambient
trusted_endpoint: false
allow_private_endpoint: false
allow_insecure_endpoint: false
max_bytes: 10485760
max_objects: 10000
max_pages: 100
page_size: 1000
policy:
max_chunk_chars: 4000
retain_published_generations: 3
The implementation must preserve these existing engine defaults exactly:
| Field | Default |
|---|---|
filesystem patterns |
["**/*.md"] |
per-object max_bytes |
10 * 1024 * 1024 |
| HTTP connect/read timeout | 5000 ms / 30000 ms |
| HTTP redirects/cache | 5 / 64 * 1024 * 1024 bytes |
| S3 objects/pages/page size | 10000 / 100 / 1000 |
max_chunk_chars |
4000 |
retain_published_generations |
3 |
P1 deliberately covers the configuration-only path for all three already-supported engine source types: strict declaration, file-path binding, render, and parse. This is required by RF1.1/D1's “sorgente completa” and the agreed three-source P1 scope. D6's “HTTP/S3 future” boundary still applies to network acquisition, materialization, preprocessing, and operational acceptance: P1 never calls either adapter. The signed-URL loader is the narrow bridge needed to keep authenticated transport values out of Git; it is not a new HTTP auth/header protocol.
Installation variables are derived only when the selected source mode needs them:
THT_WS_<NAMESPACE>_EVIDENCE_SIGNED_URLS_FILE
THT_WS_<NAMESPACE>_EVIDENCE_ACCESS_KEY_FILE
THT_WS_<NAMESPACE>_EVIDENCE_SECRET_KEY_FILE
THT_WS_<NAMESPACE>_EVIDENCE_SESSION_TOKEN_FILE # optional
<NAMESPACE> uses the existing contractNamespace normalization. These values are absolute file paths below configured secret roots, never secret values.
Hard scope boundaries
- Do not run
tht preprocess evidence,tht evidence extract, adapter discovery/acquisition, embeddings, Qdrant writes, ACTIVE publication, corpus GC, or retention execution. - Do not create or validate
artifacts/evidence,corpus/ACTIVE, evidence points, or embedding vectors. Those belong to P2/P4/P6/P8/P9. - Do not copy the Evidence tree into current immutable descriptor snapshots. P6 owns commit-addressed materialization, realpath checks, nested symlink escape rejection, and race-safe consumption.
- Do not resolve a filesystem source against the mobile registry checkout. P1 renders the reserved future path
<registry-root>/snapshots/<commit>/workspace-content/<id>/evidence;tht config checkvalidates structure without requiring that path to exist. - Do not broaden
writeRegistryFile/commitAndPushto arbitraryworkspace-contentwrites. Curators change Evidence content through a normal Git clone; the API publishes descriptors and generated docs only. - Do not add a browser preprocessing endpoint or host render/preprocessing CLI. The backend-independent host CLI and its equivalence proof are P2; full Evidence editor UX is future work.
- Do not make
ssh_tunneloperational. Preserve the renderer's fail-closed behavior until P10. - Do not claim same-revision identity from descriptor blob equality. A content-only Evidence commit has the same descriptor blob but a different authoritative commit.
Target file map
Backend contract and registry
backend/src/workspaces/schema.ts: authoritative Evidence interfaces, Zod schemas, and cross-field invariants.backend/src/workspaces/types.ts: remove or synchronize the duplicate exported v3 shape so it cannot contradict the authoritative contract.backend/src/workspaces/git-repository.ts: fixed-argv read-only tree-at-revision assertion.backend/src/workspaces/registry.ts: contextual filesystem tree checks at publish/activate and content-only revision behavior.backend/src/workspaces/contracts.ts: Evidence installation variables and generated public README.backend/src/workspaces/bindings.ts: source-specific Evidence file binding resolution.backend/src/workspaces/diagnostics.ts: report installation-localbinding_missingwhen required Evidence files are missing/unsafe.backend/src/workspaces/runtime-renderer.ts: runtimeevidence.sourcesand vector policy mapping.backend/src/tht/tht-runner.ts: commit-addressed render context.backend/src/routes/workspaces.ts: validation/read/publish/import/export round-trip and safe errors.
Harness compatibility
harness/tht/config.py: strict Evidence runtime config, signed-URL file resolution, provenance validation.harness/tht/adapters/factory.py: consume validated HTTP transport URLs without exposing them.harness/tests/test_config_resources.py: source/policy parsing and secret masking.harness/tests/test_registry_evidence_config.py: renderer-facing runtime contract and config-check behavior.
Frontend compatibility
frontend/src/api/workspaces.ts: canonical Evidence types and conflict field allowlist.frontend/src/workspaces/drafts.ts: strict sanitizer/copy functions that preserve Evidence.frontend/src/shell/WorkspaceEditor.tsx: read-only Evidence summary; existing edits preserve the object.
Examples, docs, and gates
deploy/workspaces/example.yaml,deploy/workspaces/psd.yaml.example: generic descriptor examples.docs/contracts/workspace-evidence-v3.md: human-readable canonical contract.docs/install/local-workspace-registry.md,docs/install/server-workspace-registry.md: operator registry layout and secret boundary.docs/install/examples/workspace-bindings.env.example: file-path binding examples only.scripts/verify-workspace-install-docs.sh,scripts/test-verify-workspace-install-docs.sh: executable doc contract.scripts/p1-acceptance.sh,backend/scripts/p1-acceptance.mjs,scripts/test-p1-acceptance.sh: automated process goal.scripts/p1-manual-acceptance.sh,backend/scripts/p1-manual-acceptance.mjs,backend/scripts/p1-render-snapshot.mjs,docs/testing/p1-manual-acceptance.md: manual walkthrough and explicit production-render tooling.PROJECT_STATE.md: separate automated/manual status and retained artifact path.
Task 1: Define the optional, strict schema-v3 Evidence contract
Files:
- Modify:
backend/src/workspaces/schema.ts - Modify:
backend/src/workspaces/types.ts - Modify:
backend/test/workspaces-schema.test.ts - Modify:
backend/test/workspaces-migrate-v2-qdrant.test.ts - Modify:
backend/test/workspaces-migrate-legacy.test.ts
Interfaces:
export interface EvidencePolicy {
max_chunk_chars: number;
retain_published_generations: number;
}
export type EvidenceSource =
| {
type: "filesystem";
uri: string;
patterns: string[];
max_bytes: number;
}
| {
type: "http";
uris: string[];
authentication: "none" | "signed_urls_file";
connect_timeout_ms: number;
read_timeout_ms: number;
max_bytes: number;
max_redirects: number;
allow_private_hosts: boolean;
max_cache_bytes: number;
}
| {
type: "s3";
uri: string;
endpoint_url?: string;
region?: string;
credentials: "ambient" | "static_files";
trusted_endpoint: boolean;
allow_private_endpoint: boolean;
allow_insecure_endpoint: boolean;
max_bytes: number;
max_objects: number;
max_pages: number;
page_size: number;
};
export interface WorkspaceEvidence {
source: EvidenceSource;
policy: EvidencePolicy;
}
WorkspaceV3 gains evidence?: WorkspaceEvidence; v1/v2 remain strict and unchanged. Prefer deleting the unused duplicate WorkspaceV2/WorkspaceV3 declarations from backend/src/workspaces/types.ts and importing the authoritative schema types wherever needed. If a public compatibility reason prevents deletion, re-export the schema types instead of maintaining a second handwritten structure.
- Step 1: Write failing schema tests
Add table-driven positive tests for:
- filesystem with explicit values;
- filesystem with all defaults applied;
- HTTP
noneandsigned_urls_filemodes; - S3
ambientandstatic_filesmodes; evidenceabsent on a valid v3 workspace;- parse → canonical object →
serializeWorkspaceYaml→ parse equality.
Add table-driven negative tests for:
- absolute filesystem paths,
..,., empty/doubled segments, trailing traversal, backslashes, NUL/control characters, and another workspace's namespace; - a filesystem URI above or below the canonical root (patterns select descendants; URI itself is the exact root);
- unsupported source discriminators and unknown keys;
- credential-shaped Git fields such as
password,api_key,access_key,secret_key,session_token,signed_url,headers, andca_contents; - HTTP userinfo, query, fragment, non-HTTP schemes, duplicates after canonicalization, empty manifests, and invalid bounds;
- invalid S3 schemes, empty bucket, userinfo/query/fragment, unsafe endpoint syntax, inconsistent endpoint opt-ins, and invalid limits;
- chunk/retention values outside explicit bounds;
evidenceon schema v1/v2.
Use explicit canaries in tests and assert the resulting public error message contains a safe field path but not the canary value.
- Step 2: Run the focused test and verify RED
Run:
cd backend
npx vitest run test/workspaces-schema.test.ts
Expected: FAIL because WorkspaceV3Schema rejects evidence and the types do not exist.
- Step 3: Implement strict source schemas and defaults
In schema.ts, create .strict() Zod objects and one discriminated union. Use safe-integer validation while preserving the engine's existing lower-bound semantics:
const EvidencePolicySchema = z.object({
max_chunk_chars: z.number().int().safe().positive().default(4_000),
retain_published_generations: z.number().int().safe().min(1).default(3),
}).strict();
Define source defaults exactly as listed in the completion contract. Default the whole policy object to { max_chunk_chars: 4000, retain_published_generations: 3 }, so the canonical parsed object and serialized YAML are explicit even when the author omitted policy fields. Convert descriptor milliseconds to harness seconds only in the renderer; keep integer milliseconds in Git. Validate URLs with URL, but return sanitized Zod issues that identify the field/index rather than interpolating the rejected URL.
Add small, named helpers used by workspaceInvariants, for example:
function expectedEvidenceRoot(workspaceId: string): string {
return `workspace-content/${workspaceId}/evidence`;
}
function isNormalizedRepoRelativePath(value: string): boolean {
const parts = value.split("/");
return value.length > 0
&& !value.startsWith("/")
&& !value.includes("\\")
&& !/[\u0000-\u001f\u007f]/u.test(value)
&& parts.every((part) => part !== "" && part !== "." && part !== "..");
}
The cross-field issue must be attached to evidence.source.uri and require exact equality with expectedEvidenceRoot(workspace.workspace.id). Do not call resolve, realpath, or inspect the filesystem here. Require a nonempty, stably deduplicated patterns list; each glob must be relative, slash-normalized, free of empty/./.. segments, backslashes, and control characters, so a glob cannot escape the declared root.
For HTTP, reject userinfo/query/fragment in descriptor URIs and reject duplicates after a stable canonical form. For S3, parse the s3:// URI into a nonempty bucket and optional prefix, and keep endpoint_url, region, trust flags, and limits non-secret.
- Step 4: Preserve migration behavior
Legacy and v2→v3 migration output must omit evidence, not invent a filesystem tree. Add assertions to both migration test files. This makes migrated workspaces operational for existing sessions but not yet configured for preprocessing.
- Step 5: Run focused tests and typecheck
Run:
cd backend
npx vitest run \
test/workspaces-schema.test.ts \
test/workspaces-migrate-v2-qdrant.test.ts \
test/workspaces-migrate-legacy.test.ts
npx tsc --noEmit -p .
Expected: PASS.
- Step 6: Commit
git add backend/src/workspaces/schema.ts backend/src/workspaces/types.ts \
backend/test/workspaces-schema.test.ts \
backend/test/workspaces-migrate-v2-qdrant.test.ts \
backend/test/workspaces-migrate-legacy.test.ts
git commit -m "feat: define workspace evidence descriptor contract"
Task 2: Bind filesystem declarations to a Git tree at the same revision
Files:
- Modify:
backend/src/workspaces/git-repository.ts - Modify:
backend/src/workspaces/registry.ts - Modify:
backend/test/workspaces-git-repository.test.ts - Modify:
backend/test/workspace-registry.test.ts
Interfaces:
// Read-only. It never stages, checks out, or follows worktree symlinks.
GitWorkspaceRepository.assertTreeAtRevision(
revision: string,
repoRelativePath: string,
): Promise<void>
The helper invokes Git with a fixed argv, equivalent to:
git cat-file -t <40-hex-commit>:workspace-content/<id>/evidence
and accepts only output tree. A missing path, blob/symlink at the declared root, malformed revision, or Git failure becomes a sanitized workspace_invalid/git_unavailable registry error without command stderr or source content.
- Step 1: Write failing Git repository tests
Extend the existing temporaryRemote()-based suite. Seed one commit with:
workspaces/research.yaml
workspace-content/research/evidence/guide.md
workspace-content/other/evidence/other.md
Test that the helper:
- accepts the
researchEvidence tree at that commit; - rejects a missing path;
- rejects a blob and a Git symlink at the declared root;
- distinguishes old/new commits after a content-only Evidence change;
- uses an argv array and never passes the path through a shell.
Do not recursively reject a symlink nested inside the tree. Add an explicit test/comment that nested containment is intentionally deferred to P6.
- Step 2: Run the Git test and verify RED
cd backend
npx vitest run test/workspaces-git-repository.test.ts
Expected: FAIL because assertTreeAtRevision is missing.
- Step 3: Implement the fixed-argv read-only helper
Reuse the repository's existing Git runner and revision/path safety helpers. Do not add workspace-content to isRegistryArtifactPath, writeRegistryFile, the publish staging allowlist, or generated API artifacts.
- Step 4: Write failing registry tests for contextual validation
Add real-bare-repository cases proving:
- publication succeeds only when the descriptor's filesystem tree already exists in the pulled base commit;
- the resulting publication commit contains both the descriptor and the unchanged Evidence tree;
- activation validates the tree against the exact
safeHeadused for the descriptor; - a remote descriptor with a missing/non-tree source fails pull and retains the previously active snapshot;
- a content-only remote commit creates a new
WorkspaceRevision.commitand immutable descriptor snapshot even when the descriptor blob is unchanged; - a stale API update based on the pre-content commit receives
workspace_stale/409 semantics rather than overwriting the curator's content commit; - retained and pinned historical revisions remain distinguishable by commit.
Assertions must compare commit IDs and Git object existence, not mutable checkout paths.
- Step 5: Run the registry test and verify RED
cd backend
npx vitest run test/workspace-registry.test.ts
Expected: at least the missing-tree and content-only revision cases FAIL.
- Step 6: Add one contextual validation function in the registry
Use a private helper such as:
private async assertEvidenceContext(
workspace: WorkspaceDescriptor,
revision: string,
): Promise<void> {
if (workspace.workspace.schema_version !== 3) return;
if (workspace.evidence?.source.type !== "filesystem") return;
await this.repository.assertTreeAtRevision(revision, workspace.evidence.source.uri);
}
Call it:
- after pull/read of the base and before descriptor publication;
- during activation against
safeHead, before replacing the active revision; - during integrity repair/reload wherever an existing snapshot is re-associated with a Git commit.
Publication still stages only workspaces/<id>.yaml and generated workspace-docs/<id>/.... Activation still snapshots only descriptor/contract/README/manifest. Document the deliberate P6 boundary adjacent to the code.
- Step 7: Run focused tests
cd backend
npx vitest run \
test/workspaces-git-repository.test.ts \
test/workspace-registry.test.ts
npx tsc --noEmit -p .
Expected: PASS, including the content-only revision regression.
- Step 8: Commit
git add backend/src/workspaces/git-repository.ts backend/src/workspaces/registry.ts \
backend/test/workspaces-git-repository.test.ts backend/test/workspace-registry.test.ts
git commit -m "feat: bind evidence trees to registry revisions"
Task 3: Resolve HTTP/S3 credentials from installation-local files only
Files:
- Modify:
backend/src/workspaces/contracts.ts - Modify:
backend/src/workspaces/bindings.ts - Modify:
backend/src/workspaces/diagnostics.ts - Read/verify wiring:
backend/src/app.ts - Modify:
backend/test/workspaces-contracts.test.ts - Modify:
backend/test/workspaces-bindings.test.ts - Modify:
backend/test/workspaces-diagnostics.test.ts - Modify:
backend/test/routes-sessions.test.ts - Modify:
harness/tht/config.py - Modify:
harness/tht/adapters/factory.py - Modify:
harness/tests/test_config_resources.py - Create:
harness/tests/test_registry_evidence_config.py
Backend interfaces:
export type InstallationRole = /* existing roles */ | "EVIDENCE";
export type InstallationSuffix =
| /* existing suffixes */
| "SIGNED_URLS_FILE"
| "ACCESS_KEY_FILE"
| "SECRET_KEY_FILE"
| "SESSION_TOKEN_FILE";
export interface ResolvedEvidenceBinding {
values: Record<string, string>; // safe file paths only
missing: string[];
}
export interface RuntimeBindings {
// existing roles...
evidence: ResolvedEvidenceBinding;
}
resolveEvidenceBinding(workspace, env, secretRoots) returns no variables/missing values for filesystem, HTTP none, or S3 ambient. It requires the signed-URL file for HTTP signed_urls_file; it requires access-key and secret-key files for S3 static_files, and accepts the session-token file only when present and safe.
Harness runtime shape for signed HTTP:
evidence:
sources:
- type: http
provenance_urls:
- https://evidence.example.test/guide.md
signed_urls_file: /run/secrets/example-evidence-signed-urls
# limits follow
The file is UTF-8 JSON with a nonempty array of strings, maximum 1 MiB. load_config replaces the file reference in memory with urls: list[SecretStr]; it verifies one-to-one order and canonical_provenance_uri(signed_url) == provenance_urls[index]. It never stores the file contents in the Pydantic repr, validation message, CLI output, or returned public metadata.
- Step 1: Write failing backend contract/binding tests
Cover:
-
stable namespace and exact variable names;
-
no Evidence variables for modes without file credentials;
-
source-specific variables only (HTTP never gets S3 files and vice versa);
-
HTTP signed file required;
-
S3 access/secret required together; session token optional;
-
absolute readable regular files under a configured realpath secret root accepted;
-
relative paths, missing files, directories, unreadable files, and symlink escapes rejected as
missing; -
binding results contain file paths, never file contents;
-
generated contract/README and diagnostic errors do not contain secret canaries;
-
/workspaces/:id/testreports localbinding_missingwithout changing the canonical registry revision; -
real session admission through
buildApp/routes-sessionsrefuses before Pi spawn when a declared required Evidence file is missing/unsafe, accepts a safe binding far enough to reach the existing next admission boundary, and preserves no-Evidence compatibility; -
schema-v3 DWH/vector/embedding behavior remains unchanged.
-
Step 2: Run backend tests and verify RED
cd backend
npx vitest run \
test/workspaces-contracts.test.ts \
test/workspaces-bindings.test.ts \
test/workspaces-diagnostics.test.ts \
test/routes-sessions.test.ts
Expected: FAIL because Evidence is not an installation role and RuntimeBindings has no Evidence binding.
- Step 3: Implement conditional contract generation and binding resolution
Keep the existing principle from bindings.ts: validate paths and pass them through, but never read their contents. Add resolveEvidenceBinding rather than overloading the DWH/vector transport resolver with a source type that is not one of those transports.
Update all RuntimeBindings construction sites/tests. supportsSessionRuntime must return false when a descriptor-selected Evidence credential file is missing/unsafe, while an absent Evidence section yields an empty successful binding and preserves RNF3 session compatibility. Prove the existing app.ts admission closure actually applies that result in routes-sessions.test.ts; registry publication/activation remains installation-independent and must not read local bindings. In V3 /workspaces/:id/test diagnostic preflight, convert missing required Evidence bindings to existing sanitized binding_missing diagnostics with the descriptor field (evidence.source.authentication or evidence.source.credentials) and variable name; never include an environment value.
- Step 4: Run backend tests and typecheck
cd backend
npx vitest run \
test/workspaces-contracts.test.ts \
test/workspaces-bindings.test.ts \
test/workspaces-diagnostics.test.ts \
test/routes-sessions.test.ts
npx tsc --noEmit -p .
Expected: PASS.
- Step 5: Write failing harness config tests
In test_registry_evidence_config.py, build raw runtime YAML for:
- filesystem with a deliberately nonexistent absolute root (parse/check succeeds; no adapter construction);
- public HTTP URLs;
- signed HTTP with a valid JSON secret file and matching provenance;
- signed HTTP with missing/oversized/malformed/non-list files;
- signed HTTP with reordered, extra, query-free mismatch, userinfo, or duplicate canonical provenance;
- S3 ambient credentials;
- S3
access_key_file,secret_key_file, and optionalsession_token_fileresolved intoSecretStr; - all source/policy defaults and non-default values;
- unknown Evidence keys rejected.
Capture config model repr, tht config check stdout/stderr, and exception text; assert no signed query/access key/secret key/session token canary occurs.
- Step 6: Run harness tests and verify RED
cd harness
.venv/bin/pytest -q \
tests/test_config_resources.py \
tests/test_registry_evidence_config.py
Expected: signed URL file cases FAIL because the loader only knows scalar password/access/secret/session *_file fields.
- Step 7: Implement bounded signed-URL file loading and strict Evidence models
Add a dedicated loader before Pydantic validation; do not teach the generic scalar secret resolver to parse arbitrary JSON. The outline is:
def _resolve_http_signed_url_files(value: Any) -> Any:
# Recurse only through mappings/lists.
# For {type: "http", signed_urls_file: ...}:
# lstat/stat/read at most 1 MiB as UTF-8
# parse JSON array[str]
# set urls to the array and remove signed_urls_file
# Never interpolate array values in ConfigError.
...
HttpEvidenceSourceConfig accepts provenance_urls for the file-backed form, holds actual transport urls as SecretStr, validates the one-to-one canonical mapping, and exposes a method returning secret values only to the adapter factory. Add model_config = {"extra": "forbid"} to the three modern source models, EvidenceSourcesConfig, and the policy model involved in this contract so renderer typos fail loudly.
Do not continue formatting raw ValidationError with f"{e}" after secret files have been resolved: Pydantic may include rejected input. Add a safe formatter based on e.errors(include_input=False, include_url=False) that retains only location, stable error type, and a custom message that never interpolates transport URLs/credential values. Apply it to load_config and prove existing non-secret diagnostics remain useful.
The factory may unwrap transport URLs only at the last moment when constructing HttpManifestEvidenceSource; config check must not construct any Evidence adapter or touch network/source roots.
- Step 8: Run focused harness tests and lint
cd harness
.venv/bin/pytest -q \
tests/test_config_resources.py \
tests/test_registry_evidence_config.py
.venv/bin/ruff check \
tht/config.py \
tht/adapters/factory.py \
tests/test_config_resources.py \
tests/test_registry_evidence_config.py
Expected: PASS.
- Step 9: Commit
git add \
backend/src/workspaces/contracts.ts \
backend/src/workspaces/bindings.ts \
backend/src/workspaces/diagnostics.ts \
backend/test/workspaces-contracts.test.ts \
backend/test/workspaces-bindings.test.ts \
backend/test/workspaces-diagnostics.test.ts \
backend/test/routes-sessions.test.ts \
harness/tht/config.py \
harness/tht/adapters/factory.py \
harness/tests/test_config_resources.py \
harness/tests/test_registry_evidence_config.py
git commit -m "feat: bind evidence credentials through local files"
Task 4: Render Evidence through the production runtime handoff
Files:
- Modify:
backend/src/workspaces/runtime-renderer.ts - Modify:
backend/src/tht/tht-runner.ts - Modify:
backend/test/workspace-runtime-renderer.test.ts - Modify:
backend/test/workspace-runtime-handoff.test.ts
Renderer context:
Extend the current explicit context, rather than reading the registry checkout or ambient cwd:
export interface RuntimeRenderContext {
// existing identity, roots, semantic resources...
revisionContentRoot: string; // <registry-root>/snapshots/<commit>
}
For a filesystem URI, render:
runtime_identity:
workspace_id: example
workspace_revision: <40-hex-commit>
evidence:
sources:
- type: filesystem
root: <registry-root>/snapshots/<commit>/workspace-content/example/evidence
patterns: ["**/*.md"]
max_bytes: 10485760
vector:
max_chunk_chars: 4000
retain_published_generations: 3
HTTP mapping:
- descriptor
authentication: none→ harnessurlscontaining the public descriptoruris; - descriptor
authentication: signed_urls_file→provenance_urlsplussigned_urls_filefromRuntimeBindings.evidence; - convert
_msdescriptor timeouts to exact seconds without lossy rounding; - map all limits/SSRF policy fields.
S3 mapping:
- split canonical
s3://bucket/prefixintobucketandprefix; - map endpoint/region/trust/limits;
- ambient mode emits no credential keys;
- static mode emits only
access_key_file,secret_key_file, and an optionalsession_token_filepath.
If evidence is absent, omit evidence and its policy override. Preserve all existing roots, DWH/Qdrant/Ollama behavior and the intentional ssh_tunnel error.
- Step 1: Write failing renderer tests
Add exact parsed-YAML assertions for:
-
filesystem root under the commit directory, never under
<registry>/repo; -
public/signed HTTP;
-
ambient/static S3;
-
default and non-default policy;
-
no-Evidence omission;
-
missing required Evidence bindings rejected before rendering;
-
runtime_identity.workspace_revisionequal to the directory commit; -
no secret file contents in YAML;
-
two renders with identical inputs are byte-identical;
-
a content-only commit changes identity/root even when descriptor YAML is unchanged;
-
existing
ssh_tunnelfail-closed test remains unchanged. -
Step 2: Run renderer test and verify RED
cd backend
npx vitest run test/workspace-runtime-renderer.test.ts
Expected: FAIL because no Evidence/runtime content root is rendered.
- Step 3: Implement pure renderer mapping
Keep path derivation lexical and deterministic:
const filesystemRoot = join(
context.revisionContentRoot,
workspace.evidence.source.uri,
);
This is safe only because Task 1 canonicalized the URI and Task 2 checked it as a tree at the same commit. Do not realpath it or require existence in P1.
Do not access process.env inside the renderer. Receive already validated binding file paths through RuntimeBindings so the backend has one deterministic runtime path. Treat the exact descriptor→rendered-YAML mapping and golden handoff tests as P2's compatibility contract; do not claim that the future host CLI can import this backend TypeScript module or that RF1.3 is complete before P2 supplies its backend-independent caller.
- Step 4: Write failing production handoff tests
Extend workspace-runtime-handoff.test.ts using its real local bare Git fixture and harness invocation. Test:
- a registry revision with descriptor plus Evidence tree is activated;
ThtRunner.acquireWorkspaceRuntime(snapshotPath)reads canonical descriptor/identity and passes<snapshot-dir>asrevisionContentRoot;- its leased runtime YAML has the exact Evidence source/policy mapping;
harness/.venv/bin/tht config check -c <leased-config>exits zero;- acquire/check/release twice yields identical copied YAML while lease file names may differ;
- release removes only the owned lease file;
- signed HTTP/S3 file paths resolve from configured secret roots and no canary reaches captured output.
Use the exact CLI ordering:
harness/.venv/bin/tht config check -c /absolute/path/to/rendered.yaml
Never place -c before config check.
- Step 5: Run handoff test and verify RED
cd backend
npx vitest run test/workspace-runtime-handoff.test.ts
Expected: new Evidence assertions FAIL.
- Step 6: Pass the immutable render context from
ThtRunner
readCanonicalWorkspaceSnapshot already proves that the descriptor path is under <snapshots>/<commit>/ and matches runtime_identity. Derive revisionContentRoot from that validated path/commit and pass it to the renderer. Never consult the live checkout after the snapshot is acquired.
- Step 7: Run focused cross-layer tests and checks
cd backend
npx vitest run \
test/workspace-runtime-renderer.test.ts \
test/workspace-runtime-handoff.test.ts
npx tsc --noEmit -p .
npm run build
Expected: PASS.
- Step 8: Commit
git add \
backend/src/workspaces/runtime-renderer.ts \
backend/src/tht/tht-runner.ts \
backend/test/workspace-runtime-renderer.test.ts \
backend/test/workspace-runtime-handoff.test.ts
git commit -m "feat: render revision-bound evidence configuration"
Task 5: Preserve Evidence across registry docs, HTTP routes, conflicts, and exports
Files:
- Modify:
backend/src/workspaces/contracts.ts - Modify:
backend/src/workspaces/registry.ts - Modify:
backend/src/routes/workspaces.ts - Modify:
backend/test/workspaces-contracts.test.ts - Modify:
backend/test/workspace-registry.test.ts - Modify:
backend/test/routes-workspaces.test.ts
Public artifact rule: Generated docs describe the source type, canonical non-secret URI(s), limits/policy, same-revision rule, and required installation file variable names. They never include secret contents. Export remains exactly:
manifest.json
workspace.yaml
contract.env.example
README.md
P1 does not include workspace-content bytes in the browser/API ZIP.
- Step 1: Write failing contract and registry artifact tests
Assert for all three sources:
- contract ordering and generated README are deterministic;
- only applicable variables are present;
- filesystem docs explain same-revision Git ownership and P6 materialization boundary;
- HTTP/S3 docs explain file/ambient credential modes without sample secrets;
- snapshot descriptor/contract/README/manifest hashes match the active commit;
- the snapshot directory contains no copied Evidence tree;
- a secret canary present only in a fixture file never appears in Git blobs, generated docs, snapshot metadata, or error messages.
Run and confirm RED where the generated docs omit Evidence:
cd backend
npx vitest run \
test/workspaces-contracts.test.ts \
test/workspace-registry.test.ts
- Step 2: Extend generated public documentation
Render a compact Evidence source section from canonical descriptor fields. Never read binding files while generating it. Preserve current stable ordering so re-publication of the same descriptor/base remains idempotent.
If registry snapshot integrity lists expected files, keep the current allowlist deliberately descriptor-only and add a comment pointing to P6 rather than adding workspace-content now.
- Step 3: Write failing real-route tests
Using app.inject() route tests plus the real registry fixture, cover:
/workspaces/validatereturns the canonical Evidence defaults and conditional contract;- publish create/update, pull, list, and read preserve the whole descriptor;
- an Evidence-only concurrent edit reports a safe conflict field such as
evidence.source.uriorevidence.policy.max_chunk_chars; - invalid absolute/traversal/cross-workspace/protocol/credential payloads return safe 400
workspace_invalid, do not mutate HEAD, and do not echo canaries; - contextual missing Git tree fails publish/pull safely;
- export, safe extraction, and import preserve canonical descriptor/docs;
- extracted file bytes/hashes are stable across two exports;
- ZIP contains no Evidence bytes and no secrets.
Do not require raw ZIP byte equality unless production ZIP metadata is explicitly fixed; the P1 determinism contract is stable extracted files and manifest hashes.
- Step 4: Run route test and verify RED
cd backend
npx vitest run test/routes-workspaces.test.ts
Expected: Evidence route/export expectations FAIL until all payload/artifact paths use the new canonical schema and docs.
- Step 5: Make the smallest route/artifact changes
Prefer existing validateCanonicalWorkspace, serializeWorkspaceYaml, recursive conflict folding, and exportBundle paths. Do not create a parallel Evidence DTO and do not add an Evidence upload/preprocess route. Keep public errors on the current sanitized WorkspaceRegistryError path.
- Step 6: Run focused backend tests and typecheck
cd backend
npx vitest run \
test/workspaces-contracts.test.ts \
test/workspace-registry.test.ts \
test/routes-workspaces.test.ts
npx tsc --noEmit -p .
Expected: PASS.
- Step 7: Commit
git add \
backend/src/workspaces/contracts.ts \
backend/src/workspaces/registry.ts \
backend/src/routes/workspaces.ts \
backend/test/workspaces-contracts.test.ts \
backend/test/workspace-registry.test.ts \
backend/test/routes-workspaces.test.ts
git commit -m "feat: preserve evidence in workspace artifacts"
Task 6: Keep existing browser workspace flows lossless without adding authoring UX
Files:
- Modify:
frontend/src/api/workspaces.ts - Modify:
frontend/src/api/workspaces.test.ts - Modify:
frontend/src/workspaces/drafts.ts - Modify:
frontend/src/workspaces/drafts.test.ts - Modify:
frontend/src/shell/WorkspaceEditor.tsx - Modify:
frontend/src/shell/WorkspaceEditor.test.tsx
Scope: The browser must accept and preserve canonical Evidence returned by the backend. P1 does not add source-edit controls or launch preprocessing. A small read-only summary prevents the field from being invisible while the registry descriptor remains its authoring surface.
- Step 1: Write failing frontend contract tests
Add one fixture for each source type and assert:
-
sanitizeCanonicalWorkspaceaccepts the new top-level key and returns a deep sanitized copy; -
all unknown keys, secret-shaped keys, unsafe URIs, invalid policy values, and malformed unions are rejected rather than passed into browser state;
-
draft save/load preserves Evidence;
-
API validate/read/publish/conflict parsing preserves Evidence;
-
conflict fields under
evidence.source.*andevidence.policy.*are accepted by the sanitized allowlist; -
changing an existing DWH/LLM editor field and publishing does not drop or mutate Evidence;
-
no-Evidence workspaces continue to work.
-
Step 2: Run focused tests and verify RED
cd frontend
npx vitest run \
src/workspaces/drafts.test.ts \
src/api/workspaces.test.ts \
src/shell/WorkspaceEditor.test.tsx
Expected: FAIL because exactRecord currently rejects the evidence top-level key.
- Step 3: Add explicit frontend source types and strict copy helpers
Mirror the backend wire contract in CanonicalWorkspace without importing server code into the frontend build. Add focused helpers such as copyEvidencePolicy, copyFilesystemEvidence, copyHttpEvidence, and copyS3Evidence; use the same bounds and lexical checks as defense-in-depth.
Update the top-level sanitizer allowlist:
const source = exactRecord(value, [
"workspace", "dwh", "semantic_index", "llm_policy", "diagnostics", "evidence",
]);
Return ...(evidence ? { evidence } : {}) in the sanitized object. Add the complete stable Evidence field paths to conflictFields; do not accept arbitrary server-provided conflict paths.
- Step 4: Add a read-only editor summary
When Evidence exists, show source type, safe canonical URI/count, chunk size, and retention with copy such as “Evidence is managed by the registry descriptor in P1.” Never render a signed URL or secret-file binding—those are not descriptor fields. Existing immutable updates already spread the workspace; add the regression test before relying on that behavior.
- Step 5: Run tests and typecheck
cd frontend
npx vitest run \
src/workspaces/drafts.test.ts \
src/api/workspaces.test.ts \
src/shell/WorkspaceEditor.test.tsx
npx tsc -b
Expected: PASS.
- Step 6: Commit
git add \
frontend/src/api/workspaces.ts \
frontend/src/api/workspaces.test.ts \
frontend/src/workspaces/drafts.ts \
frontend/src/workspaces/drafts.test.ts \
frontend/src/shell/WorkspaceEditor.tsx \
frontend/src/shell/WorkspaceEditor.test.tsx
git commit -m "fix: preserve workspace evidence in browser drafts"
Task 7: Document and mechanically verify the shared-registry Evidence contract
Files:
- Modify:
deploy/workspaces/example.yaml - Modify:
deploy/workspaces/psd.yaml.example - Create:
docs/contracts/workspace-evidence-v3.md - Modify:
docs/install/local-workspace-registry.md - Modify:
docs/install/server-workspace-registry.md - Modify:
docs/install/examples/workspace-bindings.env.example - Modify:
scripts/verify-workspace-install-docs.sh - Modify:
scripts/test-verify-workspace-install-docs.sh
Required documented repository layout:
registry.git/
├── workspaces/
│ ├── example.yaml
│ └── another.yaml
├── workspace-content/
│ ├── example/evidence/...
│ └── another/evidence/...
└── workspace-docs/
├── example/{contract.env.example,README.md}
└── another/{contract.env.example,README.md}
Correct any current prose that claims generated .env.example/.md files live directly under workspaces/; production writes them under workspace-docs/<id>/.
- Step 1: Add failing verifier self-tests
Create mutated fixture copies that must fail when they:
- omit the
workspace-content/<id>/evidencelayout or same-commit rule; - show an absolute/cross-workspace Evidence path;
- place generated docs in the wrong registry directory;
- omit HTTP/S3 file credential boundaries;
- contain credential literals, signed query examples, or unsafe placeholder values;
- claim P1 materializes/extracts/indexes Evidence;
- omit the exact
tht config check -c <path>ordering; - omit separate automated/manual acceptance states.
Also retain all existing adversarial doc-verifier cases.
- Step 2: Run self-test and verify RED
bash scripts/test-verify-workspace-install-docs.sh
Expected: new mutation cases are not detected yet.
- Step 3: Update generic descriptors and the canonical contract document
Add the explicit filesystem section to:
deploy/workspaces/example.yamlwithworkspace-content/example/evidence;deploy/workspaces/psd.yaml.exampleusing its generic fixture ID and matching namespace.
Do not add real PSD/client content or secrets to ThothII.
docs/contracts/workspace-evidence-v3.md must include:
-
exact strict shapes/defaults for filesystem, public/signed HTTP, and ambient/static S3;
-
safe/unsafe URI examples;
-
installation file formats and variable naming;
-
one Git repo for all workspace namespaces;
-
descriptor/tree commit identity and content-only revision semantics;
-
browser/export behavior;
-
optional Evidence/no-Evidence compatibility;
-
P1 lexical/tree checks versus P6 materialization/symlink checks;
-
exact
tht config check -ccommand; -
explicit statement that P1 does no acquisition, extraction, embeddings, Qdrant writes, ACTIVE publication, or GC.
-
Step 4: Update local/server operator guides and binding example
Describe curator flow in the right order:
- clone/pull shared registry;
- place source content under the workspace namespace and commit/push it;
- validate/publish descriptor against that base commit;
- inspect generated public docs;
- provision any
*_FILEpaths outside Git below allowed secret roots; - render/check config;
- stop—preprocessing/materialization is later P2/P6.
The env example contains only non-secret values and file paths. It may use obvious non-working paths such as /run/secrets/...; never include a credential/signed URL.
- Step 5: Strengthen the verifier and run both directions
bash scripts/test-verify-workspace-install-docs.sh
bash scripts/verify-workspace-install-docs.sh --fixtures-only
Expected: both PASS; every adversarial mutation fails inside the self-test for the intended reason.
- Step 6: Commit
git add \
deploy/workspaces/example.yaml \
deploy/workspaces/psd.yaml.example \
docs/contracts/workspace-evidence-v3.md \
docs/install/local-workspace-registry.md \
docs/install/server-workspace-registry.md \
docs/install/examples/workspace-bindings.env.example \
scripts/verify-workspace-install-docs.sh \
scripts/test-verify-workspace-install-docs.sh
git commit -m "docs: define workspace evidence registry contract"
Task 8: Automated integration goal — complete P1 configuration process
Files:
- Create:
scripts/p1-acceptance.sh - Create:
backend/scripts/p1-acceptance.mjs - Create:
backend/scripts/p1-acceptance.test.mjs - Create:
scripts/test-p1-acceptance.sh - Modify:
.gitignoreonly if.artifacts/is not already ignored (it currently is; normally no edit) - Modify:
PROJECT_STATE.md
Public command:
./scripts/p1-acceptance.sh integration --keep
It uses Node stdlib plus the built backend's normal dependencies. It must not require Docker, frontend, DWH, Qdrant, Ollama, external network, or a real remote. It starts a real Fastify listener on 127.0.0.1 with OS-assigned port 0 and makes actual HTTP requests with fetch; app.inject() does not satisfy this gate.
Exact run topology:
.artifacts/p1-integration/<run-id>/
├── ownership.json
├── remote.git/
├── author/
├── installation/
│ ├── registry/
│ ├── data/
│ ├── runtime/
│ └── bindings.env
├── fixture-secrets/ # sole secret-scan exclusion
├── fixtures/
│ ├── descriptors/
│ └── requests/
├── requests/
├── responses/
├── exports/
│ ├── raw/
│ └── extracted/
├── rendered/
├── logs/
├── report.json
└── report.md
ownership.json is written before creating child resources and includes schema version, run ID, random nonce, absolute root, repository root, start time, current PID, owned listener identity, and the exact resources the run may delete/stop. Never store tokens, secret contents, or full signed URLs in ownership/report files.
- Step 1: Write failing acceptance-runner unit tests
Use node:test for library-level guards. Test:
- run ID/root validation accepts only a direct child of the repository's canonical
.artifacts/p1-integration; - cleanup refuses a missing/malformed/mismatched ownership file, wrong nonce, symlink root, parent root, manual-acceptance root, and foreign sibling;
- cleanup removes one correctly owned synthetic run and nothing else;
- report schema requires a single result per check, no duplicate/retry attempt field, safe relative artifact paths, hashes, timestamps, command names, and
overallderived from checks; - injected failure records exactly one failed scenario, retains its run for diagnosis, and exits nonzero;
- secret scanner skips only
fixture-secretsand detects canaries everywhere else, including JSON/Markdown/logs/responses/rendered/export files; - successful non-
--keepcleanup and successful--keepretention; - no command helper accepts shell strings; Git/tht/backend commands use argv arrays.
Provide a test-only P1_ACCEPTANCE_FAIL_AT=<check-id> hook. It is not a retry mechanism; it deterministically proves failure reporting.
- Step 2: Run the runner tests and verify RED
bash scripts/test-p1-acceptance.sh
Expected: FAIL because the acceptance runner does not exist.
- Step 3: Implement preflight and owned lab lifecycle
scripts/p1-acceptance.sh must:
- resolve repository root from the script location;
- require
node,npm,git, and an executableharness/.venv/bin/tht(or an explicitTHT_BINoverride); - run
npm --prefix backend run buildonce; - invoke
node backend/scripts/p1-acceptance.mjs integration [--keep]; - preserve the Node exit code.
The Node runner must:
- create a cryptographically random run ID/nonce;
- use
mkdir-exclusive semantics and refuse reuse; - write files atomically where they are process evidence;
- use
spawn/execFilewith argv and bounded timeouts; - execute each scenario exactly once;
- always close its owned Fastify instance in
finally; - retain a failed run unconditionally;
- delete a successful run only when
--keepis absent and ownership validation passes.
Do not poll/restart a failed scenario. Awaiting app.listen() or a bounded /health readiness probe is startup synchronization, not a scenario retry; record it separately.
- Step 4: Create the local Git registry from zero
Inside the run:
git init --bare --initial-branch=main <root>/remote.git
git clone <root>/remote.git <root>/author
Configure fixture-only author identity. In the author clone, create non-secret curated trees for at least the filesystem workspace and push the bootstrap commit:
workspace-content/p1-filesystem/evidence/guide.md
workspace-content/p1-filesystem/evidence/domain/table.md
The API, not the fixture writer, publishes workspaces/*.yaml and workspace-docs/*. Additional HTTP/S3 descriptor fixtures may share the same repository but do not pretend to be acquired.
Create file bindings under fixture-secrets/ for:
- the normal DWH config required by the renderer/config loader;
- one signed-HTTP JSON list containing a unique query canary;
- S3 access/secret/session values containing distinct canaries.
Set only file-path environment bindings and configure THT_WORKSPACE_SECRET_ROOTS to the fixture secret directory. Provision the required DWH transport/host/port/user/password-file variables separately for the P1_FILESYSTEM, P1_HTTP, and P1_S3 contract namespaces (they may reference one shared fixture password file); then add the source-specific Evidence variables. Capture a redacted bindings.env containing paths and non-secret endpoints, not values.
- Step 5: Start production backend boundaries and use real HTTP
Import loadConfig, buildApp, WorkspaceRegistry, and ThtRunner from backend/dist. Build them with the run's remote/root/data/runtime settings and pass those production instances to buildApp. Listen on 127.0.0.1:0; save only the loopback base URL.
Perform and persist each request/response once:
GET /workspace-registry/status;- positive
POST /workspaces/validatefor filesystem, signed HTTP, and static-file S3 descriptors; POST /workspaces/publishcreate for each descriptor, based on the current commit returned by the preceding step/read;POST /workspace-registry/pull;GET /workspaces/:idand list;GET /workspaces/:id/exportfor each workspace;- safe extraction with the production ZIP dependency and manifest/file hash verification.
Use a sequential current-base workflow; do not blindly replay a stale base after each publication. Every recorded response must be parsed/sanitized before entering report.json.
- Step 6: Prove one immutable Git identity
For the filesystem workspace, assert and report hashes for:
API revision.commit
installation registry checkout HEAD
snapshot manifest commit
runtime_identity.workspace_revision
All four must be identical. Then use fixed-argv Git object checks:
git cat-file -e <commit>:workspaces/p1-filesystem.yaml
git cat-file -e <commit>:workspace-content/p1-filesystem/evidence/guide.md
git cat-file -t <commit>:workspace-content/p1-filesystem/evidence
The last output must be tree. Also assert the immutable snapshot contains descriptor/docs/manifest but not a materialized workspace-content tree.
Fast-forward the curator clone to the API publication HEAD, then create and push a content-only update to guide.md; invoke the registry pull once and prove:
- active revision changes to the new commit;
- descriptor blob stays equal;
- runtime identity/root changes to the new commit;
- old retained snapshot remains immutable.
This is the regression that prevents blob identity from masquerading as revision identity.
- Step 7: Exercise production rendering and the real harness check
For each workspace snapshot:
- call the production
ThtRunner.acquireWorkspaceRuntime; - copy the lease YAML into
rendered/<workspace>-1.yaml; - execute
harness/.venv/bin/tht config check -c <lease>exactly once; - release the lease;
- repeat the acquire/check as an explicit determinism/idempotence check, not a retry;
- copy
<workspace>-2.yamland compare bytes; - assert identity/source/policy fields and that all leased files were released.
The HTTP/S3 commands parse file credentials but never construct adapters or touch network. The filesystem root is allowed not to exist because P6 has not materialized it. Any Evidence acquisition call is a gate failure.
- Step 8: Execute negative scenarios with no mutation/no leak
Send separate validate requests for:
- absolute, traversal, backslash, and cross-workspace filesystem URIs;
- unsupported source type/protocol;
- descriptor credential field containing a canary;
- HTTP userinfo/query canary;
- malformed policy/limits.
For contextual validation, use a separate invalid workspace/branch state whose canonical filesystem path is absent at the referenced commit; pull/publish must fail and preserve the last valid active snapshot. Do not damage and repair the primary scenario as a hidden retry.
For every negative case assert:
-
expected safe status/code/field;
-
Git HEAD/snapshot state unchanged where applicable;
-
response/log/report contains no rejected canary or Git stderr.
-
Step 9: Verify export/docs/determinism/secrets/cleanup and write reports
The final report has stable check IDs including at least:
preflight
clean_state
ownership
local_git_bootstrap
http_validate_publish_pull_read_export
same_revision_git_objects
content_only_revision
snapshot_and_docs
runtime_render_determinism
tht_config_check
negative_schema_cases
negative_context_case
no_p1_scope_artifacts
secret_scan
cleanup_confinement
no_p1_scope_artifacts asserts that the run contains no artifacts/evidence, corpus/ACTIVE, embedding output, Qdrant records, or preprocessing invocation.
Scan every regular file below the run except fixture-secrets/ for all fixture canaries and known credential values. Scan Git blobs reachable from the remote, extracted ZIPs, requests/responses, logs, YAML, JSON, and Markdown. File paths and safe variable names are allowed; values are not.
Write report.json atomically, then derive report.md from it. End with exactly:
automated integration: PASS
manual acceptance: PENDING
when all checks pass. With --keep, print the absolute retained run path. Without --keep, validate ownership and clean only that run after printing/writing the successful result.
- Step 10: Run acceptance-runner tests
bash -n scripts/p1-acceptance.sh scripts/test-p1-acceptance.sh
bash scripts/test-p1-acceptance.sh
Expected: PASS.
- Step 11: Run the complete automated process once from clean state
Before running, verify no previous command is active and do not reuse a run root:
./scripts/p1-acceptance.sh integration --keep
Expected: exit 0; output names one new retained run; its report.json has overall: "PASS"; report.md says automated PASS/manual PENDING; no scenario has a retry/attempt count greater than one.
If it fails: stop. Diagnose from the retained run, add/fix a regression test and implementation, then invoke a new full run with a new ID. Do not rerun the same failed scenario blindly and do not overwrite the old report.
- Step 12: Inspect retained evidence and update project state
Manually inspect the report plus a sample descriptor, Git object proof, snapshot manifest, generated README, extracted export, and the two rendered configs. Record the retained relative run path and:
automated integration: PASS
manual acceptance: PENDING
in PROJECT_STATE.md. Do not mark manual acceptance complete.
- Step 13: Commit
git add \
scripts/p1-acceptance.sh \
backend/scripts/p1-acceptance.mjs \
backend/scripts/p1-acceptance.test.mjs \
scripts/test-p1-acceptance.sh \
PROJECT_STATE.md
git commit -m "test: prove P1 configuration process end to end"
Task 9: Build the independent manual-acceptance tooling
Files:
- Create:
scripts/p1-manual-acceptance.sh - Create:
backend/scripts/p1-manual-acceptance.mjs - Create:
backend/scripts/p1-manual-acceptance.test.mjs - Create:
backend/scripts/p1-render-snapshot.mjs - Create:
backend/scripts/p1-render-snapshot.test.mjs - Create:
scripts/test-p1-manual-acceptance.sh - Create:
docs/testing/p1-manual-acceptance.md - Modify after human approval only in Task 11:
PROJECT_STATE.md
Public lifecycle:
./scripts/p1-manual-acceptance.sh prepare
./scripts/p1-manual-acceptance.sh serve
./scripts/p1-manual-acceptance.sh stop
./scripts/p1-manual-acceptance.sh cleanup
The helper supports only those four lifecycle actions. It uses the fixed, independent root .artifacts/manual-acceptance/p1/ and backend address http://127.0.0.1:8791. It never reads or copies an automated integration run.
Manual topology:
.artifacts/manual-acceptance/p1/
├── ownership.json
├── backend.pid # only while served
├── remote.git/
├── author/
├── installation/
├── fixture-secrets/
├── fixtures/
├── requests/
├── responses/
├── exports/
├── rendered/
├── logs/
├── commands/ # generated concrete reviewer commands
├── GUIDE.md
└── VERDICT.md # created by reviewer, never by automation
The generated render commands use this tracked, acceptance-only interface (not an HTTP route and not the future P2 host renderer):
node backend/scripts/p1-render-snapshot.mjs \
--ownership .artifacts/manual-acceptance/p1/ownership.json \
--snapshot <absolute-commit-addressed-snapshot.yaml> \
--output .artifacts/manual-acceptance/p1/rendered/runtime-1.yaml
The script imports the built production ThtRunner, reconstructs its non-secret settings from the owned manual installation, resolves descriptor bindings from the command environment, acquires one runtime lease, copies it atomically with mode 0600, and releases the lease in finally. It accepts only owned snapshot/output paths under the fixed manual root; it never starts a backend, calls a render HTTP route, or reads secret contents itself.
- Step 1: Write failing lifecycle guard tests
Test without approving the gate:
-
preparerefuses a pre-existing root, a symlink root, automated-run input, or missing prerequisites; -
preparecreates fresh ownership, bare remote, author clone/content commit, installation directories, descriptor/request fixtures, secret files, output directories, commands, and guide; -
serverefuses unowned state, an occupied127.0.0.1:8791, an existing live PID, a stale/mismatched PID, or any non-loopback bind; -
stopsignals only the PID whose ownership nonce, executable, cwd/root, and recorded start identity match; -
cleanuprefuses while the owned server is live and removes only the exact owned fixed root after stop; -
foreign siblings and
.artifacts/p1-integrationare never removed; -
no lifecycle action writes
VERDICT.mdor changes manual status to PASS; -
the generated render commands fail safely before rendering when the saved read response is missing/malformed, its snapshot path escapes the owned installation, or its revision differs from the published Git commit;
-
p1-render-snapshot.mjsrejects unowned/symlink/out-of-root snapshot or output paths, copies one production lease, always releases it on success/failure, writes mode0600, and produces byte-identical outputs for two identical invocations without leaving runtime lease files. -
Step 2: Run lifecycle tests and verify RED
bash scripts/test-p1-manual-acceptance.sh
Expected: FAIL because the helper does not exist.
- Step 3: Implement guarded preparation and backend-only serving
prepare must:
- require that Task 8 has been implemented, but not consume its state;
- build backend once;
- create the fixed root exclusively and write ownership first;
- initialize a new bare remote and curator clone;
- seed a fresh filesystem Evidence tree and secret-file fixtures;
- create concrete positive/negative JSON request files;
- generate
commands/render-1.shandrender-2.shthat, after the reviewer has saved the successful read response, extractrevision.snapshotPathwith a bounded Node JSON parser, verify its commit equals the saved API/Git commit and that it lies below the owned installation snapshot root, then invokebackend/scripts/p1-render-snapshot.mjswith concrete owned output paths/environment; also generate safe scripts for Git inspection,diff, config checks, ZIP extraction/manifest verification, and secret scanning; - generate
GUIDE.mdwith absolute/concrete paths and expected safe outcomes; - leave the server stopped and status
PENDING.
serve must start only node backend/dist/server.js with the lab's environment, bind exactly 127.0.0.1:8791, redirect stdout/stderr to owned logs, atomically persist PID/start identity, and perform one bounded health readiness wait. It must not start Docker or frontend.
stop must validate ownership/process identity before sending TERM, wait a bounded interval, and report if the operator must intervene; it must never fall back to a broad pkill. cleanup must apply the same root/nonce/symlink checks as Task 8.
- Step 4: Write the permanent manual guide
docs/testing/p1-manual-acceptance.md explains prerequisites, four lifecycle commands, separation from automated state, expected outputs, how to preserve a failed lab, and the verdict format. It must state that the reviewer—not the helper—performs and judges the walkthrough.
The generated GUIDE.md must contain this ordered checklist:
- inspect
ownership.json, the pre-publication Evidence tree, fixture descriptor, and binding paths; - run
serveand verify only127.0.0.1:8791listens; - personally execute real
curlstatus → validate → publish → pull → read → export calls, saving each response; - only after publish, use
git log,git ls-tree, andgit show <published-commit>:workspaces/<id>.yamlplusgit show <published-commit>:workspace-content/<id>/evidence/...to inspect descriptor/content identity at that one commit; - inspect generated
workspace-docs, immutable descriptor snapshot, and snapshot manifest; - safely extract ZIP and verify manifest hashes and absence of Evidence bytes/secrets;
- run the generated production render command twice and
diffthe YAML; - inspect runtime identity, absolute reserved filesystem root, Evidence limits, and policy;
- personally execute
harness/.venv/bin/tht config check -c <rendered-1.yaml>and the second config; - submit invalid absolute/traversal/cross-workspace/protocol/credential requests and verify safe rejection/no mutation/no canary;
- run the generated secret scan outside
fixture-secrets; - confirm no preprocessing, Evidence materialization, embedding, Qdrant, ACTIVE, or retention artifact exists;
- run
stopand confirm the PID/port are gone; - record
VERDICT.mdwith reviewer, UTC time, every checklist result, observations, and eithermanual acceptance: PASSormanual acceptance: FAIL.
The guide must not tell the reviewer to inspect raw secret file contents. It may verify file ownership/mode and canary absence outside the excluded directory.
- Step 5: Run lifecycle tests and syntax checks
bash -n \
scripts/p1-manual-acceptance.sh \
scripts/test-p1-manual-acceptance.sh
bash scripts/test-p1-manual-acceptance.sh
Expected: PASS. This proves tooling only; it does not approve manual acceptance.
- Step 6: Commit the manual gate tooling
git add \
scripts/p1-manual-acceptance.sh \
backend/scripts/p1-manual-acceptance.mjs \
backend/scripts/p1-manual-acceptance.test.mjs \
backend/scripts/p1-render-snapshot.mjs \
backend/scripts/p1-render-snapshot.test.mjs \
scripts/test-p1-manual-acceptance.sh \
docs/testing/p1-manual-acceptance.md
git commit -m "test: add P1 manual configuration walkthrough"
Task 10: Re-run branch-wide verification and hand off explicit gate status
Files:
- Modify only if status/path changes:
PROJECT_STATE.md
This task runs after all implementation/tooling commits and immediately before the human walkthrough. It must leave manual acceptance PENDING.
- Step 1: Run the complete backend gate
cd backend
npx vitest run
npx tsc --noEmit -p .
npm run build
Expected: all tests, typecheck, and build PASS.
- Step 2: Run the complete harness gate
cd harness
.venv/bin/pytest -q
.venv/bin/ruff check .
Expected: PASS under the repository's default marker selection. Do not enable L2 or external services for P1.
- Step 3: Run the complete frontend gate
cd frontend
npx vitest run
npx tsc -b
npm run build
Expected: PASS.
- Step 4: Run every root verifier/tooling test
bash scripts/test-verify-workspace-install-docs.sh
bash scripts/verify-workspace-install-docs.sh --fixtures-only
bash scripts/test-p1-acceptance.sh
bash scripts/test-p1-manual-acceptance.sh
bash -n \
scripts/p1-acceptance.sh \
scripts/p1-manual-acceptance.sh \
scripts/test-p1-acceptance.sh \
scripts/test-p1-manual-acceptance.sh
Expected: PASS.
- Step 5: Execute one final clean automated run at branch HEAD
./scripts/p1-acceptance.sh integration --keep
Expected: a new run ID, exit 0, all report checks PASS, secret scan PASS, cleanup confinement PASS, and manual acceptance: PENDING. No human walkthrough has occurred in this plan sequence yet.
Do not reuse Task 8's run as the final evidence after later commits. If this command fails, follow the diagnose/fix/new-clean-run rule; never loop it automatically.
- Step 6: Inspect status and diff integrity
git diff --check
git status --short --branch
git log --oneline --decorate -12
Expected:
git diff --checkexits zero;- no
.artifactsfile is tracked; - no secret/example canary is present in tracked files;
- implementation commits are small and correspond to plan tasks;
PROJECT_STATE.mdnames the latest retained automated run and reports manual status truthfully.
If only the retained run path/status changed, commit that state:
git add PROJECT_STATE.md
git commit -m "docs: finalize P1 verification status"
- Step 7: Report completion with two independent gates
The pre-walkthrough handoff must state, on separate lines:
automated integration: PASS — <relative retained report path>
manual acceptance: PENDING — awaiting the independent reviewer walkthrough
Also state explicitly:
- P1 proves configuration, same-revision Git identity, rendering, and harness parsing;
- P1 does not prove acquisition/materialization/preprocessing/indexing/ACTIVE/GC;
- P6 is the next required step before filesystem Evidence can be consumed;
- P2/P4/P9/P10 retain their documented responsibilities.
Do not summarize P1 as fully accepted when manual status is PENDING or FAIL.
Task 11: Manual acceptance gate — descriptor and rendered configuration artifacts
Files:
-
Read:
.artifacts/manual-acceptance/p1/GUIDE.md -
Create by reviewer only:
.artifacts/manual-acceptance/p1/VERDICT.md -
Modify after explicit human approval only:
PROJECT_STATE.md -
Step 1: Stop and request the human checkpoint
Only after Task 10 reports automated PASS at branch HEAD, ask the reviewer to execute:
./scripts/p1-manual-acceptance.sh prepare
./scripts/p1-manual-acceptance.sh serve
# follow .artifacts/manual-acceptance/p1/GUIDE.md in full
./scripts/p1-manual-acceptance.sh stop
This is a controlled, resumable pause. If access/session is interrupted, leave the owned root intact, rerun only serve if the guide says no stateful scenario has begun, or use cleanup then prepare for a genuinely fresh walkthrough. Never infer approval from automated outputs.
- Step 2: Record the human result without hiding failures
If VERDICT.md says FAIL or is absent, keep:
automated integration: PASS
manual acceptance: PENDING (or FAIL with observation)
in PROJECT_STATE.md and preserve the manual root for diagnosis.
Only if the reviewer explicitly records PASS, update PROJECT_STATE.md with reviewer/date and:
automated integration: PASS
manual acceptance: PASS
Then optionally clean the lab after the reviewer confirms artifacts are no longer needed:
./scripts/p1-manual-acceptance.sh cleanup
Commit only the project-state update, never .artifacts:
git add PROJECT_STATE.md
git commit -m "docs: record P1 manual acceptance"
Requirement traceability
| Requirement/decision | Implemented/proven by |
|---|---|
| RF1.1 / D1 complete source + policy | Tasks 1, 3, 4, 5, 7 |
| RF1.2 / RNF1 no secrets in Git | Tasks 1, 3, 5, 7, 8 |
| RF1.3 same runtime rendering | P1 prerequisite only: Task 4 and Task 8 prove backend session render → harness parse; P2 must prove its backend-independent host render is equivalent before RF1.3 is complete |
| RF1.4 three DWH transports represented | Existing descriptor/contract retained; SSH remains fail-closed for P10; regression tests Tasks 3–4 |
| RF1.5 one registry, namespace isolation, same revision | Tasks 1–2 and Git-object proof Task 8 |
| RNF2 deterministic/idempotent relevant outputs | Tasks 4–5, Task 8 repeated read/render/config-check/content-only revision |
| RNF3 workspace without Evidence still works | Task 1 optional field and Tasks 4/6 regressions |
| RNF4 workspace isolation | Lexical namespace + exact Git tree checks Tasks 1–2; runtime point filtering remains outside P1 |
| RNF5 renderer compatibility | Task 4 production handoff and harness check |
| RNF7 one canonical path | Schema/renderer only; no parallel fixture contract |
| RNF8 integration-first | Automated Task 8 and branch-wide Task 10 before human Task 11 |
| D6/P6 boundary | No materialization/realpath/symlink claim; reserved commit root only |
| D9 policy defaults | Tasks 1 and 4; execution/GC remains P9 |
| Automated acceptance standard | Task 8 clean state, no retry, reports, secret scan, cleanup |
| Manual acceptance standard | Task 9 independent tooling plus Task 11 explicit resumable human checkpoint |
Execution notes
- Every task is red → green → focused verification → commit. Do not batch several red tasks into one implementation change.
- Use existing local bare-Git fixtures and production interfaces rather than mocks where a boundary already exists.
- A test-only fake is acceptable only for an external dependency that P1 explicitly excludes; the core P1 path itself must remain real Git, real Fastify HTTP, production registry/renderer, and real harness CLI.
- Keep failed acceptance artifacts. Fix the cause with a regression test, then start a new clean run. “Try it again” is not a diagnostic step.
- Human approval is a durable decision, not a command exit code. The manual helper must never create a PASS verdict.