From 659b8f251430fc45e4bcf9ce7672e55accdb91ee Mon Sep 17 00:00:00 2001 From: july <2151119264@qq.com> Date: Thu, 20 Aug 2026 11:37:56 +0800 Subject: [PATCH 1/3] feat(packaging): add Antigravity plugin artifact Co-authored-by: Codex (GPT 5.6 Sol) codex@openai.com --- .../adapters/contributing-new-coding-agent.md | 4 +- ...26-08-19-67-antigravity-plugin-artifact.md | 343 ++++ references/agent-customize/platforms/codex.md | 2 +- references/agent-customize/platforms/qoder.md | 2 +- references/project-harness/observability.md | 2 +- .../antigravity/build-antigravity-plugin.mjs | 613 +++++++ .../antigravity/plugin-manifest.json | 3 + .../antigravity/verify-antigravity-plugin.mjs | 1403 +++++++++++++++ .../antigravity-plugin-artifact.test.mjs | 1500 +++++++++++++++++ 9 files changed, 3867 insertions(+), 5 deletions(-) create mode 100644 docs/specs/2026-08-19-67-antigravity-plugin-artifact.md create mode 100644 scripts/packaging/antigravity/build-antigravity-plugin.mjs create mode 100644 scripts/packaging/antigravity/plugin-manifest.json create mode 100644 scripts/packaging/antigravity/verify-antigravity-plugin.mjs create mode 100644 test/plugins/antigravity-plugin-artifact.test.mjs diff --git a/docs/adapters/contributing-new-coding-agent.md b/docs/adapters/contributing-new-coding-agent.md index e7352670..ee44d9d3 100644 --- a/docs/adapters/contributing-new-coding-agent.md +++ b/docs/adapters/contributing-new-coding-agent.md @@ -232,7 +232,7 @@ been verified. Use a host-specific adapter page only when the matrix's split triggers are met. Before commit or review, use the -[Change Traceability Review](../../.agents/skills/change-traceability-review/SKILL.md) +[Change Traceability Review](https://github.com/QoderAI/better-harness/blob/main/.agents/skills/change-traceability-review/SKILL.md) in Review Readiness Check mode. The pull request should state: - the host/version and primary contract evidence; @@ -242,7 +242,7 @@ in Review Readiness Check mode. The pull request should state: - privacy, compatibility, generated-file, rollback, and residual risks; - AI involvement and the human verification performed. -Use the repository [pull request template](../../.github/pull_request_template.md). +Use the repository [pull request template](https://github.com/QoderAI/better-harness/blob/main/.github/pull_request_template.md). Do not infer Story ids, AI involvement, CI status, or native compatibility from branch names, prose, passing synthetic tests, or similarity to another host. diff --git a/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md b/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md new file mode 100644 index 00000000..8f81ca66 --- /dev/null +++ b/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md @@ -0,0 +1,343 @@ +# Package Better Harness for Antigravity + +## Traceability + +- Spec ID: antigravity-plugin-artifact +- Story: #67 +- Status: Implemented +- Scope: implemented static buildable and verifiable Antigravity plugin artifact + slice; native Antigravity integration and support remain unverified. + +## Intent + +Implement the smallest static source, build, verification, and publication slice +that produces a self-contained Better Harness plugin-shaped artifact for +Antigravity. The implementation does not establish native Antigravity install, +discovery, selection, invocation, compatibility, distribution, or first-class +support. + +The work follows the maintainer-authorized direction in +[Issue #67](https://github.com/QoderAI/better-harness/issues/67). The issue +conversation records phodal, a repository MEMBER, responding "Welcome to PR". +That comment authorizes a reviewable contribution; it is not a merge, release, +compatibility, Marketplace, or support commitment. + +## Pinned Evidence + +- The implementation base is Better Harness commit + [`842a6b5070d401f983693d4c4d6a83b7ad1841c6`](https://github.com/QoderAI/better-harness/tree/842a6b5070d401f983693d4c4d6a83b7ad1841c6). +- The pinned [architecture owner](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/docs/ARCHITECTURE.md) + places source-local artifact assembly under `scripts/packaging/` and treats + generated artifacts as outputs rather than canonical product owners. +- The existing [generic host builder](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/scripts/packaging/build-host-plugin.mjs), + [generic verifier](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/scripts/packaging/verify-host-plugin.mjs), + and [Codex artifact tests](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/test/plugins/host-plugin-artifact.test.mjs) + remain separate owners and regression evidence. +- The target-specific authority is the official + [Antigravity CLI Plugins documentation](https://www.antigravity.google/docs/cli/plugins), + observed 2026-08-20 under Antigravity CLI v1.1.14. Its full manifest schema + requires string `name`, permits optional string `description`, and closes + additional properties. The page's pattern is implemented as + `^[A-Za-z0-9_-]+$`, following its stated alphanumeric, hyphen, and underscore + intent without treating `9-_` as a character range. It also documents + `agy plugin install`; that command was not run locally. +- The official [Antigravity Plugins documentation](https://www.antigravity.google/docs/plugins), + observed 2026-08-19 under generic Antigravity 2.0 v2.8.1, supplies shared + plugin-root and Skill-layout context. Its optional-name behavior is not the + contract for the Agy CLI v1.1.14 profile implemented here. +- The official [Antigravity Skills documentation](https://www.antigravity.google/docs/skills), + observed on the same date and generic documentation version, confirms that a + Skill is a folder containing `SKILL.md`. The CLI page shows a plugin `skills/` + directory but does not independently prove discovery or invocation of this + artifact's nested Skill in a native CLI build. + +The official pages are live rather than commit-addressed. Their observation +date and displayed documentation version bound the facts consumed here; static +documentation does not prove behavior in a selected IDE or CLI build. + +## Official Contract Boundary + +The Agy CLI v1.1.14 profile relies on these target-specific facts: + +1. A plugin root contains required `plugin.json`. +2. The manifest is an object closed to `name` and optional `description`. +3. `name` is a required string using only ASCII alphanumerics, hyphens, and + underscores, enforced as `^[A-Za-z0-9_-]+$`. +4. `description`, when present, is a string; an empty string remains valid. +5. `agy plugin install` is a documented command, not evidence that installation + or invocation succeeded in this environment. + +The generic 2.0 pages supply shared layout evidence only. Their optional-name +default is deliberately excluded from this CLI profile. The CLI page includes +an example `$schema` editor hint, while its listed full JSON Schema closes +properties to `name` and `description`; this frozen verifier follows the listed +schema and rejects `$schema` as profile drift. Static conformance does not prove +native install, Skill discovery, selection, invocation, or compatibility. + +## Implemented Surfaces and Ownership + +| Surface | Owner | Implemented contract | +| --- | --- | --- | +| `scripts/packaging/antigravity/plugin-manifest.json` | Agy CLI packaging source | Exact source value `{ "name": "better-harness" }`; a valid name-only subset of the official CLI schema, source-only, and never an install root. | +| `scripts/packaging/antigravity/build-antigravity-plugin.mjs` | Antigravity artifact builder | Allowlisted assembly, dependency copy, staged verification, canonical boundary checks, and transactional publication. | +| `scripts/packaging/antigravity/verify-antigravity-plugin.mjs` | Antigravity artifact verifier | Closed tree, identity, marker, package, Markdown/runtime closure, license, bounds, and runtime smoke validation. | +| `test/plugins/antigravity-plugin-artifact.test.mjs` | Artifact conformance evidence | Positive build/replace/run coverage and adversarial identity, parser, path, race, rollback, and cleanup cases. | +| `skills/better-harness/**` and Better Harness runtime | Existing canonical capability owners | Copied and validated without redirecting, renaming, or forking product judgment. | +| Native Antigravity behavior | Antigravity | Unobserved; no approved `agy` binary or selected IDE/CLI build was exercised. | + +Canonical sources feed the generated host artifact in one direction. Generated +files never become canonical input and never write back to the repository. + +The verifier requires an own string `name` matching the CLI pattern; absence, +blank or non-string values, invalid characters, and unknown properties fail +without a basename fallback. It accepts optional string `description` because +that field is valid in the official CLI schema, while the owned source and +builder deliberately produce the narrower exact name-only subset. Manifest +name, root basename, ownership marker, and package identity must all resolve to +the Better Harness artifact contract. + +## Implemented Artifact Profile + +The generated root basename is `better-harness`. Its positive allowlist is: + +- root `plugin.json`, `.antigravity-plugin-artifact.json`, and private + `package.json`; +- `README.md`, `AGENTS.md`, `DESIGN.md`, `LICENSE`, `CHANGELOG.md`, + `CODE_OF_CONDUCT.md`, and `CONTRIBUTING.md`; +- canonical `skills/better-harness/**` only; +- `scripts/**` except `scripts/packaging/**`; +- `references/**`, `templates/**`, `models/**`, `hooks/**`, `docs/**`, and + `case-studies/**`; +- `node_modules/@vscode/tree-sitter-wasm/**` and + `node_modules/esbuild-wasm/**`, including package metadata and licenses. + +The builder copies only regular files and directories reached without following +symbolic links. The verifier rejects unknown roots, other Skills and host +shells, development/test state, caches, environment files, special files, +symlinks or junctions, traversal, canonical escape, and case or Unicode +identity collisions. + +The generated root `package.json` is a closed Better Harness schema with exactly +these keys: `name`, `version`, `private`, `license`, `type`, `bin`, `engines`, +and `dependencies`. It binds: + +- `name` to `@qoder-ai/better-harness`, `private` to `true`, and `type` to + `module`; +- `bin` to the singleton `{ "better-harness": "scripts/better-harness.mjs" }`; +- `engines` to exact nonblank `node` and `npm` entries projected from source; +- `dependencies` to exact version-bound `@vscode/tree-sitter-wasm` and + `esbuild-wasm` entries. + +The builder projects fresh nested objects and rejects missing, extra, blank, +non-string, or wrong source runtime metadata rather than aliasing source +objects into the artifact. + +## Canonical Closure and Link Integrity + +The canonical artifact entry is unchanged at +`skills/better-harness/SKILL.md`; there is no wrapper, flat Skill, redirect, +generated mirror, or copied business-logic owner. `DESIGN.md` is required +because shipped `AGENTS.md` links to it as an offline dependency. + +Every relative local Markdown link is an artifact hard dependency. It must +resolve uniquely to an allowlisted regular file inside the canonical root. +Repository-only navigation uses absolute upstream HTTPS URLs and is not copied +or traversed. `pathname:` is treated only as a non-local Docusaurus route; +`file:` and unknown schemes fail closed. + +The verified pinned artifact contains a Markdown closure of exactly 93 nodes, +290 edges, and 96 files. Runtime analysis starts at +`scripts/better-harness.mjs` and proves a syntax-aware ESM closure of 19 modules +and 39 edges, plus the exact two packaged dependencies and their licenses. +Limits bound files, bytes, depth, nodes, and edges. + +Three target-only source-integrity repair groups make that shipped closure +self-contained without changing product behavior: + +1. repository-only governance links in + `docs/adapters/contributing-new-coding-agent.md` use upstream HTTPS targets; +2. Codex and Qoder Custom Agent Review links resolve to the shared parent + reference; +3. the observability instructions link to the canonical Agent instructions + review reference. + +## Build, Boundary, and Publication Contract + +`build-antigravity-plugin.mjs` stages into a unique sibling container, assembles +only allowlisted inputs, writes the manifest and ownership marker, invokes the +full verifier, and publishes only after staged verification succeeds. + +Output boundary validation is read-only. It rejects invalid basenames, roots, +lexical repository overlap, and canonical overlap by resolving the nearest +existing ancestor and projecting missing suffixes. The first parent creation is +pinned to the approved preflight `canonicalParent`; the builder then rechecks +the original lexical output. Only a successful second check authorizes staging +under the post-check canonical parent and all destination operations against +the frozen post-check `canonicalOutput`. This prevents a parent symlink or +junction retarget from redirecting writes into the repository. + +An existing destination is only an early ownership gate. At replacement time, +the builder atomically moves the actual live destination into a unique sibling +backup container at `better-harness`, runs the full verifier on that moved tree, +and publishes the stage only if revalidation succeeds. Concurrent unowned, +corrupt, or symlink swaps are restored without publication; rollback conflicts +retain the moved tree rather than deleting it. + +Publication reports stable states: + +| Outcome | State and observability | +| --- | --- | +| New or clean replacement publication | `state="published"`, `backupCleanup="complete"`. | +| New artifact live but old backup removal failed | API resolves with `state="published"`, `backupCleanup="pending"`, warning `backup-cleanup-pending`; JSON and human CLI remain successful and expose no absolute path. | +| Destination changed and was restored | `destination-changed`, `not-published-destination-restored`. | +| Changed destination rollback conflicted | `destination-revalidation-rollback-failed`, `not-published-backup-retained`. | +| Stage publish failed and verified prior artifact was restored | `publish-failed`, `not-published-destination-restored`. | +| Stage publish rollback failed | `publish-rollback-failed`, `not-published-verified-backup-retained`. | +| Initial destination-to-backup rename failed | `publish-backup-rename-failed`, `not-published-destination-unchanged`, with bounded backup-container cleanup status. | + +Errors and warnings expose only stable codes, states, and safe backup basenames; +they do not disclose absolute private paths or claim that a partially cleaned +backup remains complete or recoverable. + +## Acceptance Scenarios + +- **AC-1 (traceability):** The implementation links Issue #67, MEMBER + contribution authorization, pinned base, implementation paths, and the + target Agy CLI v1.1.14 page plus generic layout pages with their observation + dates and distinct authority boundaries. +- **AC-2 (source manifest):** The source manifest parses to exactly + `{ "name": "better-harness" }`, a valid name-only subset of the closed CLI + schema; no optional field or native behavior is claimed by the source. +- **AC-3 (artifact root):** The builder writes the source manifest as root + `plugin.json`, the verifier requires its closed CLI `name`/`description` + schema with required patterned name, and the source directory is never an + install root. +- **AC-4 (canonical Skill):** The artifact contains only + `skills/better-harness/**` under `skills/` and preserves the canonical + `SKILL.md` without redirect or duplicate implementation. +- **AC-5 (allowlist):** Positive builds contain only the root files, runtime + roots, canonical Skill, and two dependency roots in the implemented profile. +- **AC-6 (closed exclusions):** Unknown, development, host-shell, special-file, + link, collision, traversal, and canonical-escape cases fail closed. +- **AC-7 (Markdown closure):** Verification proves the complete recursive + 93-node, 290-edge, 96-file Markdown closure and rejects missing, escaping, + ambiguous, cyclic-over-limit, and unsupported-scheme targets. +- **AC-8 (runtime closure):** Verification proves the 19-module ESM runtime + closure, exact dependencies and licenses, bounded parsing, and artifact-local + Better Harness CLI `--help` smoke before publication. +- **AC-9 (identity and replacement):** Required patterned CLI manifest name, + optional string description, root basename, ownership marker, closed package + identity, host, and schema are bound without absent-name fallback; the actual + moved destination passes full revalidation before replacement. +- **AC-10 (atomicity and concurrency):** Pre/post canonical boundary checks, + concurrent swap tests, bounded rollback states, and truthful cleanup-pending + success warnings preserve user data without accepting a partial artifact. +- **AC-11 (regression):** The generic Codex builder/verifier remain unchanged; + their focused artifact tests pass with the Antigravity suite. +- **AC-12 (product honesty):** README, support and adapter matrices, host + catalogs, root CLI, package scripts, release metadata, and generated graph + content are not widened; no native or first-class support is claimed. + +## Implemented Negative Evidence + +| Area | Covered evidence | +| --- | --- | +| Manifest and identity | Missing/malformed/non-object manifest; required-name absence, blank/non-string/pattern-invalid/wrong name; non-string description; `unexpected`, `$schema`, and other extra fields; exact name-only source; closed marker and package schemas. | +| Filesystem safety | Invalid output, lexical/canonical overlap, missing-parent zero-write rejection, pre/post junction retarget, symlink/junction/special file, traversal, collision, and bounds. | +| Closure | Missing/escaping/transitive Markdown targets, cycles and limits, syntax-authoritative ESM parsing, missing runtime modules, forbidden packaging imports, and unexpected dependencies. | +| Dependencies and licenses | Missing/wrong dependency metadata, versions, roots, imported subpaths, licenses, and non-regular inputs. | +| Ownership and publication | Unowned/corrupt destination, real moved-tree revalidation, concurrent swaps, first backup rename fault, stage rename fault, rollback conflict, and backup cleanup fault. | +| Product boundary | Frozen generic/package/product surfaces and no native-support assertion without an approved `agy` smoke. | + +## Non-goals + +- Install or invoke Antigravity, mutate a native plugin home, inspect private + host state, or select an Antigravity IDE/CLI version. +- Claim native install, discovery, selection, invocation, uninstall, + compatibility, lifecycle, configured assets, or first-class support. +- Add an adapter, session/evidence integration, host catalog entry, support + matrix row, README installation path, root CLI route, package script, + Marketplace publication, release metadata, or native install receipt. +- Change the generic builder/verifier, existing host shells, package or lock + files, generated documentation graph content, or canonical product judgment. +- Infer manifest fields beyond the target CLI's closed `name`/`description` + schema or apply the generic 2.0 optional-name default to this profile. + +## Plan and Tasks + +1. **Gate 1 - source contract (complete):** Establish the exact source manifest, + official-fact boundary, ownership, allowlist, closure, and product non-goals. +2. **Gate 2 - static build and verification (complete):** Implement the + Antigravity-specific builder, verifier, artifact tests, source link-integrity + repairs, canonical boundary checks, and transactional publication states. +3. **Gate 3 - static conformance and review readiness (complete):** Run focused, + Codex regression, doc-link, real build/replace/verify/Better CLI smoke, + package, full-repository baseline, graph, scope, and privacy checks. + +Native evidence is a later, independent, maintainer-approved contribution. It +is not a completion condition for this static artifact spec and must not widen +support claims without a selected native build and explicit review. + +## Risks and Controls + +| Residual risk | Control and current evidence | +| --- | --- | +| No approved native binary or IDE/CLI build was tested | Keep native compatibility and support unverified; require isolated install/discovery/invocation/uninstall evidence in a separate contribution. | +| Windows lacks privileges for some symlink/junction fixtures | Capability-gated focused tests skip rather than weaken production checks; independent non-link race and parser cases still run. | +| Live CLI/generic docs, upstream `main`, Issue, and PR state can drift | Bind CLI authority to v1.1.14 observed 2026-08-20, generic layout facts to 2.0 v2.8.1 observed 2026-08-19, and implementation facts to the pinned base; recheck immediately before PR creation. | +| Upstream absolute URLs do not follow forks or topic branches | Use them only for source-only governance/navigation dependencies; keep offline artifact dependencies relative and verified. | +| Handwritten syntax-aware Markdown and ESM parsers can miss new syntax | Bound traversal, fail closed on unsupported forms, assert adversarial syntax cases, and pin real closure cardinalities. | +| Static conformance may be mistaken for native support | Keep README, matrices, catalogs, native homes, Marketplace, and release surfaces unchanged. | + +## Open Questions + +- [NEEDS CLARIFICATION: Should a future native smoke install Antigravity CLI + v1.1.14 exactly or a maintainer-selected successor, and what compatibility + drift must be accepted?] +- [NEEDS CLARIFICATION: How does the selected native CLI discover and invoke + the generic-layout `skills/better-harness/SKILL.md`, which the CLI page does + not independently prove for this artifact?] +- [NEEDS CLARIFICATION: Which isolated native install, discovery, selection, + invocation, uninstall, and configured-asset evidence must pass before a + first-class support decision?] + +These questions block native-support activation, not completion of the static +buildable and verifiable artifact slice. + +## Test and Review Evidence + +- Antigravity artifact focused plus existing Codex artifact and doc-link tests: + 52 tests, 49 passed, 0 failed, and 3 Windows capability-gated skips, using the + installed offline Vitest 4.1.10 runner with the issue worktree as root. +- Real unique-output sequence: build JSON, verify JSON, artifact-local Better + Harness CLI `--help`, repeat replacement, and second verification all passed; + publication states were `published`, replacement changed from false to true, + and sibling stage/backup residual count was zero. This is a Better Harness + runtime smoke, not an `agy` native smoke. +- Full repository `npm test`, executed with the same offline runner/config: + 100 files total, 94 passed and 6 failed; 1,448 tests total, 1,420 passed, + 7 failed, and 21 skipped. Six failures were Windows `EPERM` symlink fixtures; + one governance reporter fixture expected a worktree-local + `node_modules/vitest/vitest.mjs` that the isolated worktree did not contain. + The full repository gate is therefore not green. +- `npm run pack:verify` passed after redirecting npm's cache from the denied + user cache to a writable task-local cache: npm package 546 entries and runtime + zip 575 entries. +- Builder and verifier `node --check` and direct `--help` passed. Source + manifest exactness, the three source link-integrity groups, generated package + schema, closure cardinalities, JSON output, and path redaction were inspected + through behavior tests and the real artifact. +- CLI manifest behavior accepts required valid `name` with absent, empty, or + nonempty string `description`; it rejects absent, blank, non-string, + pattern-invalid, or wrong names, non-string descriptions, malformed/non-object + JSON, and extra keys including `unexpected` and `$schema`. +- The canonical doc-link generator was run once after this reconciliation; + graph content hash remained equal to `HEAD`, with quiet content diff and zero + raw/numstat changes. The worktree may still show a same-blob line-ending `.M`. +- `git diff --check`, changed-surface privacy/absolute-local-path scan, and + status scope inspection passed. No dependency installation, native host + mutation, commit, push, or pull request occurred during implementation. + +The local environment has no approved `agy` executable. Native Antigravity +install, discovery, selection, invocation, uninstall, configured assets, IDE or +CLI compatibility, and Marketplace behavior remain unobserved. diff --git a/references/agent-customize/platforms/codex.md b/references/agent-customize/platforms/codex.md index aa7ced45..e031317b 100644 --- a/references/agent-customize/platforms/codex.md +++ b/references/agent-customize/platforms/codex.md @@ -137,7 +137,7 @@ when concurrent threads could edit the same files. Use subagents for bounded exploration, testing, triage, or independent review while the main thread owns the final decision and implementation. -Use [Custom Agent Review](custom-agents-review.md) when configured Agent +Use [Custom Agent Review](../custom-agents-review.md) when configured Agent profiles, their descriptions, prompts, tool boundaries, or inventory count need quality review. diff --git a/references/agent-customize/platforms/qoder.md b/references/agent-customize/platforms/qoder.md index 453d4077..18e101e9 100644 --- a/references/agent-customize/platforms/qoder.md +++ b/references/agent-customize/platforms/qoder.md @@ -206,7 +206,7 @@ Official scenario examples: > Official documentation: https://docs.qoder.com/extensions/subagent -Use [Custom Agent Review](custom-agents-review.md) for provider-neutral content +Use [Custom Agent Review](../custom-agents-review.md) for provider-neutral content quality, tool-boundary, count, and report-summary checks. This section owns the Qoder file format and invocation examples only. diff --git a/references/project-harness/observability.md b/references/project-harness/observability.md index a67dde60..fa084f34 100644 --- a/references/project-harness/observability.md +++ b/references/project-harness/observability.md @@ -93,7 +93,7 @@ entry/exit logs across every function. ## What Belongs in AGENTS.md -Load [agents-md-review.md](agents-md-review.md) when instructions are in scope. +Load [agents-md-review.md](../agent-customize/agents-md-review.md) when instructions are in scope. Keep only non-inferable facts: - actual logging facade and architecture link; diff --git a/scripts/packaging/antigravity/build-antigravity-plugin.mjs b/scripts/packaging/antigravity/build-antigravity-plugin.mjs new file mode 100644 index 00000000..8c3ce834 --- /dev/null +++ b/scripts/packaging/antigravity/build-antigravity-plugin.mjs @@ -0,0 +1,613 @@ +#!/usr/bin/env node + +import { createRequire } from "node:module"; +import { + copyFile, + lstat, + mkdir, + mkdtemp, + readFile, + realpath, + readdir, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomUUID } from "node:crypto"; + +import { + ANTIGRAVITY_ARTIFACT_KIND, + ANTIGRAVITY_ARTIFACT_MARKER, + ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + ANTIGRAVITY_HOST, + ANTIGRAVITY_PLUGIN_NAME, + CANONICAL_SKILL, + RUNTIME_DEPENDENCIES, + RUNTIME_DEPENDENCY_LICENSES, + isAllowedArtifactPath, + validatePortablePathComponent, + verifyAntigravityPluginArtifact, +} from "./verify-antigravity-plugin.mjs"; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); +const defaultRepoRoot = path.resolve(currentDir, "..", "..", ".."); +const SOURCE_MANIFEST = "scripts/packaging/antigravity/plugin-manifest.json"; +const ROOT_FILES = Object.freeze([ + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", +]); +const SOURCE_ROOTS = Object.freeze([ + "skills/better-harness", + "scripts", + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", +]); +const GENERATED_NAMES = new Set([ + ".DS_Store", + ".plugin-eval", + ".cache", + ".docusaurus", + ".vite", + "cache", + "coverage", + "dist", + "node_modules", + "outputs", + "temp", + "tmp", +]); + +export class AntigravityBuildError extends Error { + constructor(code, message, details = {}) { + super(message); + this.name = "AntigravityBuildError"; + this.code = code; + Object.assign(this, details); + } +} + +function fail(code, message, details) { + throw new AntigravityBuildError(code, message, details); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isNonblankString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function isContained(candidate, parent) { + const relative = path.relative(parent, candidate); + return relative === "" || ( + relative !== ".." + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); +} + +async function readJsonObject(filePath, code) { + let value; + try { + value = JSON.parse(await readFile(filePath, "utf8")); + } catch { + fail(code, "Required source JSON is missing, unreadable, or malformed"); + } + if (!isObject(value)) fail(code, "Required source JSON must be an object"); + return value; +} + +async function requireRegularSource(filePath, canonicalRepoRoot, code) { + let stats; + try { + stats = await lstat(filePath); + } catch { + fail(code, "Required source file is missing or unreadable"); + } + if (stats.isSymbolicLink() || !stats.isFile()) fail(code, "Required source must be a regular file"); + const canonical = await realpath(filePath).catch(() => null); + if (!canonical || !isContained(canonical, canonicalRepoRoot)) { + fail(code, "Required source escapes the canonical repository root"); + } + return { stats, canonical }; +} + +function shouldSkipSource(relativePath) { + if (relativePath.startsWith("node_modules/")) return false; + if (relativePath === "scripts/packaging" || relativePath.startsWith("scripts/packaging/")) return true; + return relativePath.split("/").some((component) => ( + GENERATED_NAMES.has(component) + || component === ".env" + || component.startsWith(".env.") + || component.endsWith(".tmp") + || component.endsWith(".log") + )); +} + +async function copyRegularTree({ source, destination, relativePath, canonicalBoundary }) { + if (shouldSkipSource(relativePath)) return; + for (const component of relativePath.split("/")) validatePortablePathComponent(component); + if (!isAllowedArtifactPath(relativePath)) fail("source-path-forbidden", "Source path is outside the artifact allowlist"); + let stats; + try { + stats = await lstat(source); + } catch { + fail("source-entry-unreadable", "Allowlisted source entry is missing or unreadable"); + } + if (stats.isSymbolicLink()) fail("source-symlink-forbidden", "Source artifact input must not be a symbolic link"); + if (!stats.isDirectory() && !stats.isFile()) fail("source-special-file-forbidden", "Source artifact input must be regular"); + const canonical = await realpath(source).catch(() => null); + if (!canonical || !isContained(canonical, canonicalBoundary)) { + fail("source-path-escape", "Source artifact input escapes its canonical root"); + } + if (stats.isFile()) { + await mkdir(path.dirname(destination), { recursive: true }); + await copyFile(source, destination); + return; + } + await mkdir(destination, { recursive: true }); + const entries = await readdir(source, { withFileTypes: true }).catch(() => { + fail("source-directory-unreadable", "Allowlisted source directory cannot be read"); + }); + entries.sort((left, right) => left.name.localeCompare(right.name, "en")); + for (const entry of entries) { + const childRelative = `${relativePath}/${entry.name}`; + if (shouldSkipSource(childRelative)) continue; + await copyRegularTree({ + source: path.join(source, entry.name), + destination: path.join(destination, entry.name), + relativePath: childRelative, + canonicalBoundary, + }); + } +} + +export function artifactPackage(sourcePackage) { + const binKeys = isObject(sourcePackage.bin) ? Object.keys(sourcePackage.bin) : []; + const engineKeys = isObject(sourcePackage.engines) ? Object.keys(sourcePackage.engines).sort() : []; + if ( + sourcePackage.name !== "@qoder-ai/better-harness" + || !isNonblankString(sourcePackage.version) + || !isNonblankString(sourcePackage.license) + || sourcePackage.type !== "module" + || !isObject(sourcePackage.dependencies) + ) { + fail("source-package-invalid", "Source package identity or runtime metadata is invalid"); + } + if ( + binKeys.length !== 1 + || binKeys[0] !== "better-harness" + || sourcePackage.bin["better-harness"] !== "scripts/better-harness.mjs" + ) { + fail("source-bin-invalid", "Source package bin must match the artifact runtime entry"); + } + if ( + engineKeys.length !== 2 + || engineKeys[0] !== "node" + || engineKeys[1] !== "npm" + || !isNonblankString(sourcePackage.engines.node) + || !isNonblankString(sourcePackage.engines.npm) + ) { + fail("source-engines-invalid", "Source package engines must contain nonblank node and npm ranges"); + } + const dependencyKeys = Object.keys(sourcePackage.dependencies).sort(); + const expected = [...RUNTIME_DEPENDENCIES].sort(); + if ( + dependencyKeys.length !== expected.length + || dependencyKeys.some((dependency, index) => dependency !== expected[index]) + || RUNTIME_DEPENDENCIES.some((dependency) => !isNonblankString(sourcePackage.dependencies[dependency])) + ) { + fail("source-dependencies-invalid", "Source package dependencies do not match the artifact profile"); + } + return { + name: sourcePackage.name, + version: sourcePackage.version, + private: true, + license: sourcePackage.license, + type: sourcePackage.type, + bin: { "better-harness": sourcePackage.bin["better-harness"] }, + engines: { node: sourcePackage.engines.node, npm: sourcePackage.engines.npm }, + dependencies: Object.fromEntries(RUNTIME_DEPENDENCIES.map((dependency) => ( + [dependency, sourcePackage.dependencies[dependency]] + ))), + }; +} + +async function resolveDependencyRoot(repoRoot, dependency) { + const require = createRequire(path.join(repoRoot, "package.json")); + let packageJsonPath; + try { + packageJsonPath = require.resolve(`${dependency}/package.json`); + } catch { + fail("dependency-resolution-failed", "Required runtime dependency cannot be resolved"); + } + const packageRoot = path.dirname(packageJsonPath); + const canonicalRoot = await realpath(packageRoot).catch(() => null); + if (!canonicalRoot) fail("dependency-resolution-failed", "Runtime dependency root cannot be resolved"); + return { packageRoot, canonicalRoot }; +} + +async function copyDependency({ repoRoot, stageRoot, dependency, expectedVersion }) { + const { packageRoot, canonicalRoot } = await resolveDependencyRoot(repoRoot, dependency); + const metadata = await readJsonObject(path.join(packageRoot, "package.json"), "dependency-package-invalid"); + if (metadata.name !== dependency || metadata.version !== expectedVersion) { + fail("dependency-identity-invalid", "Resolved dependency identity or version does not match source package.json"); + } + await requireRegularSource( + path.join(packageRoot, RUNTIME_DEPENDENCY_LICENSES[dependency]), + canonicalRoot, + "dependency-license-invalid", + ); + await copyRegularTree({ + source: packageRoot, + destination: path.join(stageRoot, "node_modules", ...dependency.split("/")), + relativePath: `node_modules/${dependency}`, + canonicalBoundary: canonicalRoot, + }); +} + +export async function assertOutputBoundary(repoRoot, outputRoot) { + if ( + typeof outputRoot !== "string" + || !outputRoot + || outputRoot.includes("\0") + || path.basename(path.resolve(outputRoot)) !== ANTIGRAVITY_PLUGIN_NAME + ) { + fail("output-invalid", "Output must be a path whose basename is better-harness"); + } + const resolvedRepo = path.resolve(repoRoot); + const resolvedOutput = path.resolve(outputRoot); + const resolvedParent = path.dirname(resolvedOutput); + if (resolvedOutput === path.parse(resolvedOutput).root) fail("output-overlap", "Output must not be a filesystem root"); + if (isContained(resolvedOutput, resolvedRepo) || isContained(resolvedRepo, resolvedOutput)) { + fail("output-overlap", "Output must not overlap the repository source root"); + } + const canonicalRepo = await realpath(resolvedRepo).catch(() => null); + if (!canonicalRepo) fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + + const missingComponents = []; + let existingAncestor = resolvedParent; + while (true) { + let stats; + try { + stats = await lstat(existingAncestor); + } catch (error) { + if (error?.code !== "ENOENT") { + fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + } + const parent = path.dirname(existingAncestor); + if (parent === existingAncestor) { + fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + } + missingComponents.unshift(path.basename(existingAncestor)); + existingAncestor = parent; + continue; + } + if (!stats.isDirectory() && !stats.isSymbolicLink()) { + fail("output-boundary-unavailable", "Output parent boundary must be a directory"); + } + break; + } + const canonicalAncestor = await realpath(existingAncestor).catch(() => null); + if (!canonicalAncestor) fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + const canonicalAncestorStats = await lstat(canonicalAncestor).catch(() => null); + if (!canonicalAncestorStats?.isDirectory()) { + fail("output-boundary-unavailable", "Output parent boundary must resolve to a directory"); + } + const canonicalParent = path.resolve(canonicalAncestor, ...missingComponents); + const canonicalOutput = path.join(canonicalParent, path.basename(resolvedOutput)); + if (isContained(canonicalOutput, canonicalRepo) || isContained(canonicalRepo, canonicalOutput)) { + fail("output-overlap", "Output must not overlap the repository source root"); + } + return { + resolvedOutput, + resolvedParent, + canonicalRepo, + canonicalParent, + canonicalOutput, + }; +} + +function publicationComplete(replaced) { + return { + state: "published", + backupCleanup: "complete", + replaced, + }; +} + +function publicationError(code, message, state, backupName, details = {}) { + fail(code, message, { + publication: { state, backupName, ...details }, + }); +} + +export async function publishStagedArtifact({ + stageRoot, + outputRoot, + replaceExisting, + operations = { rename, remove: rm }, +}) { + if (!replaceExisting) { + await operations.rename(stageRoot, outputRoot); + return { publication: publicationComplete(false), warnings: [] }; + } + const backupName = `.better-harness-antigravity-backup-${randomUUID()}`; + const backupContainer = path.join(path.dirname(outputRoot), backupName); + const backupRoot = path.join(backupContainer, ANTIGRAVITY_PLUGIN_NAME); + await mkdir(backupContainer); + try { + await operations.rename(outputRoot, backupRoot); + } catch { + let backupCleanup = "complete"; + try { + await operations.remove(backupContainer, { recursive: true, force: true }); + } catch { + backupCleanup = "pending"; + } + publicationError( + "publish-backup-rename-failed", + "Artifact publication could not begin and the destination was left unchanged", + "not-published-destination-unchanged", + backupName, + { backupCleanup }, + ); + } + + try { + await verifyAntigravityPluginArtifact(backupRoot); + } catch { + try { + await operations.rename(backupRoot, outputRoot); + await operations.remove(backupContainer, { recursive: true, force: true }).catch(() => {}); + } catch { + publicationError( + "destination-revalidation-rollback-failed", + "Destination changed after preflight and its moved tree was retained", + "not-published-backup-retained", + backupName, + ); + } + publicationError( + "destination-changed", + "Destination changed after preflight and was restored without publication", + "not-published-destination-restored", + backupName, + ); + } + + try { + await operations.rename(stageRoot, outputRoot); + } catch { + try { + await operations.rename(backupRoot, outputRoot); + await operations.remove(backupContainer, { recursive: true, force: true }).catch(() => {}); + } catch { + publicationError( + "publish-rollback-failed", + "Artifact publication failed and the verified moved tree was retained", + "not-published-verified-backup-retained", + backupName, + ); + } + publicationError( + "publish-failed", + "Artifact publication failed and the prior artifact was restored", + "not-published-destination-restored", + backupName, + ); + } + + try { + await operations.remove(backupContainer, { recursive: true, force: true }); + } catch { + const warning = { + code: "backup-cleanup-pending", + state: "published", + backupCleanup: "pending", + backupName, + }; + return { + publication: { + state: "published", + backupCleanup: "pending", + replaced: true, + backupName, + }, + warnings: [warning], + }; + } + return { publication: publicationComplete(true), warnings: [] }; +} + +async function pathExists(candidate) { + return Boolean(await lstat(candidate).catch(() => null)); +} + +export async function buildAntigravityPluginArtifact({ + repoRoot, + outputRoot, + operations = { rename, remove: rm }, + boundaryOperations = { mkdir }, +} = {}) { + if (!repoRoot || !outputRoot) fail("build-arguments-invalid", "repoRoot and outputRoot are required"); + const preflightBoundary = await assertOutputBoundary(repoRoot, outputRoot); + await boundaryOperations.mkdir(preflightBoundary.canonicalParent, { recursive: true }); + const boundary = await assertOutputBoundary(repoRoot, outputRoot); + const sourceManifestPath = path.join(boundary.canonicalRepo, ...SOURCE_MANIFEST.split("/")); + await requireRegularSource(sourceManifestPath, boundary.canonicalRepo, "source-manifest-invalid"); + const sourceManifest = await readJsonObject(sourceManifestPath, "source-manifest-invalid"); + if ( + Object.keys(sourceManifest).length !== 1 + || sourceManifest.name !== ANTIGRAVITY_PLUGIN_NAME + ) { + fail("source-manifest-invalid", "Source Antigravity manifest must equal the frozen name-only object"); + } + + let replaceExisting = false; + const destinationStats = await lstat(boundary.canonicalOutput).catch(() => null); + if (destinationStats) { + if (destinationStats.isSymbolicLink() || !destinationStats.isDirectory()) { + fail("destination-unowned", "Existing output is not a fully verified Antigravity artifact"); + } + try { + await verifyAntigravityPluginArtifact(boundary.canonicalOutput); + replaceExisting = true; + } catch { + fail("destination-unowned", "Existing output is not a fully verified Antigravity artifact"); + } + } + + const stageContainer = await mkdtemp(path.join(boundary.canonicalParent, ".better-harness-antigravity-stage-")); + const stageRoot = path.join(stageContainer, ANTIGRAVITY_PLUGIN_NAME); + await mkdir(stageRoot); + try { + const sourcePackage = await readJsonObject( + path.join(boundary.canonicalRepo, "package.json"), + "source-package-invalid", + ); + const packageJson = artifactPackage(sourcePackage); + for (const rootFile of ROOT_FILES) { + await copyRegularTree({ + source: path.join(boundary.canonicalRepo, rootFile), + destination: path.join(stageRoot, rootFile), + relativePath: rootFile, + canonicalBoundary: boundary.canonicalRepo, + }); + } + for (const sourceRoot of SOURCE_ROOTS) { + if (!(await pathExists(path.join(boundary.canonicalRepo, ...sourceRoot.split("/"))))) continue; + await copyRegularTree({ + source: path.join(boundary.canonicalRepo, ...sourceRoot.split("/")), + destination: path.join(stageRoot, ...sourceRoot.split("/")), + relativePath: sourceRoot, + canonicalBoundary: boundary.canonicalRepo, + }); + } + for (const dependency of RUNTIME_DEPENDENCIES) { + await copyDependency({ + repoRoot: boundary.canonicalRepo, + stageRoot, + dependency, + expectedVersion: packageJson.dependencies[dependency], + }); + } + await writeFile(path.join(stageRoot, "plugin.json"), `${JSON.stringify(sourceManifest, null, 2)}\n`, "utf8"); + await writeFile(path.join(stageRoot, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`, "utf8"); + await writeFile(path.join(stageRoot, ANTIGRAVITY_ARTIFACT_MARKER), `${JSON.stringify({ + kind: ANTIGRAVITY_ARTIFACT_KIND, + schemaVersion: ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + host: ANTIGRAVITY_HOST, + pluginName: ANTIGRAVITY_PLUGIN_NAME, + version: packageJson.version, + canonicalSkill: CANONICAL_SKILL, + runtimeDependencies: [...RUNTIME_DEPENDENCIES], + }, null, 2)}\n`, "utf8"); + + const verified = await verifyAntigravityPluginArtifact(stageRoot); + const published = await publishStagedArtifact({ + stageRoot, + outputRoot: boundary.canonicalOutput, + replaceExisting, + operations, + }); + await rm(stageContainer, { recursive: true, force: true }); + return { + ...verified, + replaced: replaceExisting, + publication: published.publication, + warnings: published.warnings, + }; + } catch (error) { + await rm(stageContainer, { recursive: true, force: true }).catch(() => {}); + throw error; + } +} + +export function parseArgs(argv) { + const options = { outputRoot: null, json: false, help: false }; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--out") { + if (options.outputRoot !== null) fail("argument-duplicate", "--out may be provided only once"); + const value = argv[index + 1]; + if (!value || value.startsWith("--")) fail("argument-value-missing", "--out requires a value"); + options.outputRoot = value; + index += 1; + } else if (argument.startsWith("--out=")) { + if (options.outputRoot !== null) fail("argument-duplicate", "--out may be provided only once"); + const value = argument.slice("--out=".length); + if (!value) fail("argument-value-missing", "--out requires a value"); + options.outputRoot = value; + } else if (argument === "--json") options.json = true; + else if (argument === "--help" || argument === "-h") options.help = true; + else fail("argument-unknown", "Unknown argument"); + } + if (!options.help && options.outputRoot === null) fail("argument-required", "--out is required"); + return options; +} + +export function usage() { + return [ + "Usage: node scripts/packaging/antigravity/build-antigravity-plugin.mjs --out [options]", + "", + "Options:", + " --out Output root named better-harness (required)", + " --json Emit parser-safe JSON", + " -h, --help Print help", + "", + ].join("\n"); +} + +export function formatBuildSuccess(result, { json = false } = {}) { + if (json) { + return `${JSON.stringify({ ok: true, data: result, warnings: result.warnings ?? [] }, null, 2)}\n`; + } + const summary = `Antigravity plugin artifact built: ${result.pluginName} ${result.version} (${result.fileCount} files)\n`; + if (result.publication?.backupCleanup !== "pending") return summary; + const warning = result.warnings?.find((entry) => entry.code === "backup-cleanup-pending"); + return `${summary}Warning [backup-cleanup-pending]: artifact published; state=published backupCleanup=pending (${warning?.backupName ?? "backup-retained"})\n`; +} + +export async function main(argv = process.argv.slice(2)) { + let options; + try { + options = parseArgs(argv); + if (options.help) { + process.stdout.write(usage()); + return 0; + } + const result = await buildAntigravityPluginArtifact({ + repoRoot: defaultRepoRoot, + outputRoot: options.outputRoot, + }); + process.stdout.write(formatBuildSuccess(result, { json: options.json })); + return 0; + } catch (error) { + const code = error instanceof AntigravityBuildError ? error.code : "artifact-build-failed"; + const message = error instanceof AntigravityBuildError ? error.message : "Artifact build failed"; + if (options?.json || argv.includes("--json")) { + process.stdout.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}\n`); + } else { + process.stderr.write(`Antigravity plugin build failed [${code}]: ${message}\n`); + } + return 1; + } +} + +const currentFile = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) { + process.exitCode = await main(); +} diff --git a/scripts/packaging/antigravity/plugin-manifest.json b/scripts/packaging/antigravity/plugin-manifest.json new file mode 100644 index 00000000..55b981cd --- /dev/null +++ b/scripts/packaging/antigravity/plugin-manifest.json @@ -0,0 +1,3 @@ +{ + "name": "better-harness" +} diff --git a/scripts/packaging/antigravity/verify-antigravity-plugin.mjs b/scripts/packaging/antigravity/verify-antigravity-plugin.mjs new file mode 100644 index 00000000..ca39f88c --- /dev/null +++ b/scripts/packaging/antigravity/verify-antigravity-plugin.mjs @@ -0,0 +1,1403 @@ +#!/usr/bin/env node + +import { lstat, readFile, readdir, realpath } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export const ANTIGRAVITY_ARTIFACT_KIND = "better-harness-antigravity-plugin-artifact"; +export const ANTIGRAVITY_ARTIFACT_MARKER = ".antigravity-plugin-artifact.json"; +export const ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION = 1; +export const ANTIGRAVITY_HOST = "antigravity"; +export const ANTIGRAVITY_PLUGIN_NAME = "better-harness"; +export const CANONICAL_SKILL = "skills/better-harness/SKILL.md"; +export const RUNTIME_ENTRY = "scripts/better-harness.mjs"; +export const RUNTIME_DEPENDENCIES = Object.freeze([ + "@vscode/tree-sitter-wasm", + "esbuild-wasm", +]); +export const RUNTIME_DEPENDENCY_LICENSES = Object.freeze({ + "@vscode/tree-sitter-wasm": "LICENSE", + "esbuild-wasm": "LICENSE.md", +}); +export const GRAPH_LIMITS = Object.freeze({ + markdownNodes: 128, + markdownEdges: 1024, + markdownDepth: 32, + runtimeModules: 1024, + runtimeEdges: 8192, + runtimeDepth: 64, +}); +export const FILE_LIMITS = Object.freeze({ + entries: 25_000, + directoryDepth: 64, + componentLength: 255, + files: 20_000, + totalBytes: 256 * 1024 * 1024, + textBytes: 4 * 1024 * 1024, +}); + +const ROOT_FILES = new Set([ + "plugin.json", + ANTIGRAVITY_ARTIFACT_MARKER, + "package.json", + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", +]); +const BROAD_ROOTS = new Set([ + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", +]); +const RUNTIME_ROOTS = new Set([ + "scripts", + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", +]); +const GENERATED_NAMES = new Set([ + ".DS_Store", + ".plugin-eval", + ".cache", + ".docusaurus", + ".vite", + "cache", + "coverage", + "dist", + "outputs", + "temp", + "tmp", +]); +const MARKER_KEYS = Object.freeze([ + "canonicalSkill", + "host", + "kind", + "pluginName", + "runtimeDependencies", + "schemaVersion", + "version", +]); +const CLI_MANIFEST_KEYS = new Set(["description", "name"]); +const CLI_PLUGIN_NAME_PATTERN = /^[A-Za-z0-9_-]+$/u; +const PACKAGE_KEYS = Object.freeze([ + "bin", + "dependencies", + "engines", + "license", + "name", + "private", + "type", + "version", +]); +const REQUIRED_FILES = Object.freeze([ + "plugin.json", + ANTIGRAVITY_ARTIFACT_MARKER, + "package.json", + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + CANONICAL_SKILL, + RUNTIME_ENTRY, + "node_modules/@vscode/tree-sitter-wasm/package.json", + "node_modules/@vscode/tree-sitter-wasm/LICENSE", + "node_modules/esbuild-wasm/package.json", + "node_modules/esbuild-wasm/LICENSE.md", +]); + +export class AntigravityArtifactError extends Error { + constructor(code, message) { + super(message); + this.name = "AntigravityArtifactError"; + this.code = code; + } +} + +function fail(code, message) { + throw new AntigravityArtifactError(code, message); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isNonblankString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function posixPath(value) { + return value.split(path.sep).join("/"); +} + +function isContained(candidate, parent) { + const relative = path.relative(parent, candidate); + return relative === "" || ( + relative !== ".." + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); +} + +function hasGeneratedName(relativePath) { + const names = relativePath.split("/"); + return names.some((name) => ( + GENERATED_NAMES.has(name) + || name === ".env" + || name.startsWith(".env.") + || name.endsWith(".tmp") + || name.endsWith(".log") + )); +} + +export function isAllowedArtifactPath(relativePath) { + if (typeof relativePath !== "string" || !relativePath || relativePath.includes("\0")) { + return false; + } + const normalized = path.posix.normalize(relativePath); + if ( + normalized !== relativePath + || normalized === "." + || normalized === ".." + || normalized.startsWith("../") + || path.posix.isAbsolute(normalized) + || normalized.includes("\\") + || hasGeneratedName(normalized) + ) { + return false; + } + if (!normalized.includes("/")) return ROOT_FILES.has(normalized) || [ + "skills", + "scripts", + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", + "node_modules", + ].includes(normalized); + + if (normalized === "skills/better-harness" || normalized.startsWith("skills/better-harness/")) { + return true; + } + if (normalized.startsWith("skills/")) return false; + + if (normalized === "scripts/packaging" || normalized.startsWith("scripts/packaging/")) { + return false; + } + if (normalized.startsWith("scripts/")) return true; + + const first = normalized.split("/", 1)[0]; + if (BROAD_ROOTS.has(first)) return true; + + if (normalized === "node_modules/@vscode") return true; + if ( + normalized === "node_modules/@vscode/tree-sitter-wasm" + || normalized.startsWith("node_modules/@vscode/tree-sitter-wasm/") + ) { + return true; + } + if ( + normalized === "node_modules/esbuild-wasm" + || normalized.startsWith("node_modules/esbuild-wasm/") + ) { + return true; + } + return false; +} + +function assertSafeRelative(relativePath, code = "path-unsafe") { + if (!isAllowedArtifactPath(relativePath)) { + fail(code, "Artifact contains a path outside the Antigravity allowlist"); + } +} + +function identityKey(relativePath) { + return relativePath.normalize("NFC").toLowerCase(); +} + +const WINDOWS_RESERVED_COMPONENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu; + +export function validatePortablePathComponent(component) { + if ( + typeof component !== "string" + || component.length === 0 + || component.length > FILE_LIMITS.componentLength + || /[\u0000-\u001f\u007f]/u.test(component) + || component.includes(":") + || component.includes("\\") + || component.endsWith(".") + || component.endsWith(" ") + || WINDOWS_RESERVED_COMPONENT.test(component) + ) { + fail("path-component-invalid", "Artifact contains a non-portable path component"); + } + return component; +} + +export function validateTraversalBounds(entryCount, directoryDepth) { + if (!Number.isInteger(entryCount) || entryCount < 0 || entryCount > FILE_LIMITS.entries) { + fail("artifact-entry-limit", "Artifact exceeds the fixed entry limit"); + } + if ( + !Number.isInteger(directoryDepth) + || directoryDepth < 0 + || directoryDepth > FILE_LIMITS.directoryDepth + ) { + fail("artifact-depth-limit", "Artifact exceeds the fixed directory depth limit"); + } +} + +async function inspectArtifactTree(root, canonicalRoot) { + const inventory = new Map(); + const identities = new Map(); + let fileCount = 0; + let entryCount = 0; + let totalBytes = 0; + + async function visit(directory, relativeDirectory = "", depth = 0) { + validateTraversalBounds(entryCount, depth); + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch { + fail("tree-unreadable", "Artifact tree cannot be read safely"); + } + entries.sort((left, right) => left.name.localeCompare(right.name, "en")); + for (const entry of entries) { + validatePortablePathComponent(entry.name); + entryCount += 1; + validateTraversalBounds(entryCount, depth); + const relative = relativeDirectory + ? `${relativeDirectory}/${entry.name}` + : entry.name; + assertSafeRelative(relative, "path-not-allowed"); + + const key = identityKey(relative); + if (identities.has(key) && identities.get(key) !== relative) { + fail("path-identity-collision", "Artifact contains a case or Unicode path identity collision"); + } + identities.set(key, relative); + + const absolute = path.join(root, ...relative.split("/")); + let stats; + try { + stats = await lstat(absolute); + } catch { + fail("tree-entry-unreadable", "Artifact tree contains an unreadable entry"); + } + if (stats.isSymbolicLink()) { + fail("symlink-forbidden", "Artifact must not contain symbolic links or junctions"); + } + if (!stats.isDirectory() && !stats.isFile()) { + fail("special-file-forbidden", "Artifact must contain only regular files and directories"); + } + + let canonicalEntry; + try { + canonicalEntry = await realpath(absolute); + } catch { + fail("canonical-path-unavailable", "Artifact entry cannot be canonically resolved"); + } + if (!isContained(canonicalEntry, canonicalRoot)) { + fail("canonical-path-escape", "Artifact entry escapes the canonical plugin root"); + } + inventory.set(relative, { absolute, canonical: canonicalEntry, stats }); + + if (stats.isDirectory()) { + await visit(absolute, relative, depth + 1); + } else { + fileCount += 1; + totalBytes += stats.size; + if (fileCount > FILE_LIMITS.files || totalBytes > FILE_LIMITS.totalBytes) { + fail("artifact-bounds-exceeded", "Artifact exceeds fixed file or byte limits"); + } + } + } + } + + await visit(root); + return { inventory, entryCount, fileCount, totalBytes }; +} + +function requireRegularFile(inventory, relativePath) { + const entry = inventory.get(relativePath); + if (!entry?.stats.isFile()) { + fail("required-file-missing", `Artifact is missing required regular file: ${relativePath}`); + } + return entry; +} + +async function readBoundedText(entry, code) { + if (entry.stats.size > FILE_LIMITS.textBytes) { + fail("text-file-too-large", "Artifact metadata or closure source exceeds the fixed text limit"); + } + try { + return await readFile(entry.absolute, "utf8"); + } catch { + fail(code, "Artifact metadata or closure source cannot be read"); + } +} + +async function readJsonObject(inventory, relativePath, code) { + const text = await readBoundedText(requireRegularFile(inventory, relativePath), code); + let value; + try { + value = JSON.parse(text); + } catch { + fail(code, `Artifact JSON is malformed: ${relativePath}`); + } + if (!isObject(value)) { + fail(code, `Artifact JSON must be an object: ${relativePath}`); + } + return value; +} + +export function effectivePluginName(manifest) { + if (!isObject(manifest)) { + fail("manifest-object-required", "plugin.json must contain a JSON object"); + } + if (Object.keys(manifest).some((key) => !CLI_MANIFEST_KEYS.has(key))) { + fail("manifest-schema-invalid", "Agy CLI plugin.json permits only name and description"); + } + if (!Object.hasOwn(manifest, "name")) { + fail("manifest-name-required", "Agy CLI plugin.json requires name"); + } + if (typeof manifest.name !== "string") { + fail("manifest-name-type-invalid", "Agy CLI plugin.json name must be a string"); + } + if (manifest.name.length === 0) { + fail("manifest-name-blank", "Agy CLI plugin.json name must not be empty"); + } + if (!CLI_PLUGIN_NAME_PATTERN.test(manifest.name)) { + fail("manifest-name-pattern-invalid", "Agy CLI plugin.json name contains unsupported characters"); + } + if (Object.hasOwn(manifest, "description") && typeof manifest.description !== "string") { + fail("manifest-description-invalid", "Agy CLI plugin.json description must be a string"); + } + return manifest.name; +} + +function validateMarker(marker) { + const keys = Object.keys(marker).sort(); + if (keys.length !== MARKER_KEYS.length || keys.some((key, index) => key !== MARKER_KEYS[index])) { + fail("marker-schema-invalid", "Artifact ownership marker fields do not match schema version 1"); + } + if (marker.kind !== ANTIGRAVITY_ARTIFACT_KIND) { + fail("marker-kind-invalid", "Artifact ownership marker kind is invalid"); + } + if (marker.schemaVersion !== ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION) { + fail("marker-schema-version-invalid", "Artifact ownership marker schema version is unsupported"); + } + if (marker.host !== ANTIGRAVITY_HOST) { + fail("marker-host-invalid", "Artifact ownership marker host is invalid"); + } + if (!isNonblankString(marker.pluginName)) { + fail("marker-plugin-name-invalid", "Artifact ownership marker plugin name is invalid"); + } + if (!isNonblankString(marker.version)) { + fail("marker-version-invalid", "Artifact ownership marker version is invalid"); + } + if (marker.canonicalSkill !== CANONICAL_SKILL) { + fail("marker-skill-invalid", "Artifact ownership marker canonical Skill is invalid"); + } + if ( + !Array.isArray(marker.runtimeDependencies) + || marker.runtimeDependencies.length !== RUNTIME_DEPENDENCIES.length + || marker.runtimeDependencies.some((value, index) => value !== RUNTIME_DEPENDENCIES[index]) + ) { + fail("marker-dependencies-invalid", "Artifact ownership marker runtime dependencies are invalid"); + } +} + +function validatePackage(packageJson, marker) { + const packageKeys = Object.keys(packageJson).sort(); + if ( + packageKeys.length !== PACKAGE_KEYS.length + || packageKeys.some((key, index) => key !== PACKAGE_KEYS[index]) + ) { + fail("package-schema-invalid", "Artifact package.json must use the closed Better Harness artifact schema"); + } + if (packageJson.name !== "@qoder-ai/better-harness") { + fail("package-name-invalid", "Artifact package.json name must match Better Harness"); + } + if (packageJson.private !== true) { + fail("package-private-invalid", "Artifact package.json must be private"); + } + if (!isNonblankString(packageJson.version) || packageJson.version !== marker.version) { + fail("package-version-invalid", "Artifact package version must match the ownership marker"); + } + if (!isNonblankString(packageJson.license)) { + fail("package-license-invalid", "Artifact package.json must declare a nonblank license"); + } + if (packageJson.type !== "module") { + fail("package-type-invalid", "Artifact package.json must use the module type"); + } + if ( + !isObject(packageJson.bin) + || Object.keys(packageJson.bin).length !== 1 + || Object.keys(packageJson.bin)[0] !== "better-harness" + || packageJson.bin["better-harness"] !== RUNTIME_ENTRY + ) { + fail("package-bin-invalid", "Artifact package bin must match the runtime entry exactly"); + } + const engineKeys = isObject(packageJson.engines) ? Object.keys(packageJson.engines).sort() : []; + if ( + engineKeys.length !== 2 + || engineKeys[0] !== "node" + || engineKeys[1] !== "npm" + || !isNonblankString(packageJson.engines.node) + || !isNonblankString(packageJson.engines.npm) + ) { + fail("package-engines-invalid", "Artifact package engines must contain nonblank node and npm ranges"); + } + if (!isObject(packageJson.dependencies)) { + fail("package-dependencies-invalid", "Artifact package dependencies must be an object"); + } + const keys = Object.keys(packageJson.dependencies).sort(); + const expected = [...RUNTIME_DEPENDENCIES].sort(); + if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) { + fail("package-dependencies-invalid", "Artifact package dependencies must match the runtime profile"); + } + for (const dependency of RUNTIME_DEPENDENCIES) { + if (!isNonblankString(packageJson.dependencies[dependency])) { + fail("package-dependency-version-invalid", "Artifact dependency versions must be nonblank strings"); + } + } +} + +async function validateDependencyPackages(inventory, packageJson) { + for (const dependency of RUNTIME_DEPENDENCIES) { + const relative = `node_modules/${dependency}/package.json`; + const dependencyPackage = await readJsonObject(inventory, relative, "dependency-package-invalid"); + if (dependencyPackage.name !== dependency) { + fail("dependency-package-name-invalid", "Runtime dependency package identity does not match its root"); + } + if ( + !isNonblankString(dependencyPackage.version) + || dependencyPackage.version !== packageJson.dependencies[dependency] + ) { + fail("dependency-package-version-invalid", "Runtime dependency package version does not match package.json"); + } + } +} + +function maskMarkdownCode(source) { + const output = [...source]; + let index = 0; + let fence = null; + const listStack = []; + while (index < source.length) { + const lineStart = index === 0 || source[index - 1] === "\n"; + if (lineStart) { + const lineEnd = source.indexOf("\n", index); + const end = lineEnd < 0 ? source.length : lineEnd; + const line = source.slice(index, end); + const marker = line.match(/^ {0,3}(`{3,}|~{3,})/u)?.[1]; + if (marker && (!fence || marker[0] === fence[0] && marker.length >= fence.length)) { + fence = fence ? null : marker; + for (let cursor = index; cursor < end; cursor += 1) output[cursor] = " "; + index = end; + continue; + } + if (!fence && line.trim().length > 0) { + const leading = line.match(/^ */u)?.[0].length ?? 0; + const listMarker = line.match(/^( *)(?:[-+*]|\d{1,9}[.)])([ \t]+)/u); + if (listMarker) { + const markerIndent = listMarker[1].length; + while (listStack.length && markerIndent < listStack.at(-1).contentIndent) { + listStack.pop(); + } + const parentIndent = listStack.at(-1)?.contentIndent ?? 0; + if (markerIndent - parentIndent <= 3) { + listStack.push({ markerIndent, contentIndent: listMarker[0].length }); + } + } else { + while (listStack.length && leading < listStack.at(-1).contentIndent) listStack.pop(); + const contentIndent = listStack.at(-1)?.contentIndent ?? 0; + const codeIndent = leading - contentIndent; + if ((!listStack.length && leading >= 4) || (listStack.length && codeIndent >= 4)) { + for (let cursor = index; cursor < end; cursor += 1) output[cursor] = " "; + index = end; + continue; + } + } + } + } + if (fence) { + if (source[index] !== "\n") output[index] = " "; + index += 1; + continue; + } + if (source[index] === "`") { + let ticks = 1; + while (source[index + ticks] === "`") ticks += 1; + const closing = source.indexOf("`".repeat(ticks), index + ticks); + if (closing < 0) fail("markdown-link-unsupported", "Markdown contains an unterminated code span"); + for (let cursor = index; cursor < closing + ticks; cursor += 1) { + if (source[cursor] !== "\n") output[cursor] = " "; + } + index = closing + ticks; + continue; + } + index += 1; + } + if (fence) fail("markdown-link-unsupported", "Markdown contains an unterminated code fence"); + return output.join(""); +} + +function unescapeMarkdownDestination(value) { + return value.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/gu, "$1"); +} + +function parseMarkdownDestination(source, start, terminator) { + let index = start; + while (/[ \t]/u.test(source[index] ?? "")) index += 1; + if (source[index] === "<") { + let value = ""; + index += 1; + while (index < source.length && source[index] !== ">") { + if (source[index] === "\n" || source[index] === "\r" || source[index] === "<") { + fail("markdown-link-unsupported", "Markdown contains an unsupported angle destination"); + } + if (source[index] === "\\" && index + 1 < source.length) { + value += source[index] + source[index + 1]; + index += 2; + } else { + value += source[index]; + index += 1; + } + } + if (source[index] !== ">") fail("markdown-link-unsupported", "Markdown angle destination is unterminated"); + return { target: unescapeMarkdownDestination(value), end: index + 1 }; + } + + let value = ""; + let depth = 0; + while (index < source.length) { + const char = source[index]; + if (char === "\\" && index + 1 < source.length) { + value += char + source[index + 1]; + index += 2; + continue; + } + if (char === "(" ) { + depth += 1; + value += char; + index += 1; + continue; + } + if (char === ")") { + if (depth === 0 && terminator === ")") break; + if (depth === 0) fail("markdown-link-unsupported", "Markdown destination has unbalanced parentheses"); + depth -= 1; + value += char; + index += 1; + continue; + } + if (char === "\n" || char === "\r" || (depth === 0 && /[ \t]/u.test(char))) break; + value += char; + index += 1; + } + if (depth !== 0 || !value) fail("markdown-link-unsupported", "Markdown destination is malformed"); + return { target: unescapeMarkdownDestination(value), end: index }; +} + +function finishInlineMarkdownLink(source, destinationEnd) { + let index = destinationEnd; + while (/[ \t\n\r]/u.test(source[index] ?? "")) index += 1; + if (source[index] === ")") return index + 1; + const quote = source[index]; + if (!["\"", "'", "("].includes(quote)) { + fail("markdown-link-unsupported", "Markdown link has an unsupported title or suffix"); + } + const closing = quote === "(" ? ")" : quote; + index += 1; + while (index < source.length && source[index] !== closing) { + if (source[index] === "\\") index += 2; + else index += 1; + } + if (source[index] !== closing) fail("markdown-link-unsupported", "Markdown link title is unterminated"); + index += 1; + while (/[ \t\n\r]/u.test(source[index] ?? "")) index += 1; + if (source[index] !== ")") fail("markdown-link-unsupported", "Markdown link is unterminated"); + return index + 1; +} + +function closingMarkdownLabel(source, start) { + let depth = 1; + for (let index = start; index < source.length; index += 1) { + if (source[index] === "\\") { + index += 1; + continue; + } + if (source[index] === "[") depth += 1; + if (source[index] === "]") { + depth -= 1; + if (depth === 0) return index; + } + } + return -1; +} + +function normalizeMarkdownLabel(label) { + return label + .replace(/\\([\[\]\\])/gu, "$1") + .trim() + .toLowerCase() + .replace(/\s+/gu, " "); +} + +export function parseMarkdownTargets(source) { + if (typeof source !== "string") fail("markdown-source-invalid", "Markdown source must be text"); + const masked = maskMarkdownCode(source); + const targets = []; + const definitions = new Map(); + + for (let lineStart = 0; lineStart <= masked.length;) { + const lineEnd = masked.indexOf("\n", lineStart); + const end = lineEnd < 0 ? masked.length : lineEnd; + const line = masked.slice(lineStart, end); + const originalLine = source.slice(lineStart, end); + const indentation = line.match(/^ {0,3}/u)?.[0].length ?? 0; + const definitionClose = line[indentation] === "[" + ? closingMarkdownLabel(line, indentation + 1) + : -1; + if (definitionClose >= 0 && line[definitionClose + 1] === ":") { + let destinationStart = definitionClose + 2; + while (/[ \t]/u.test(line[destinationStart] ?? "")) destinationStart += 1; + const parsed = parseMarkdownDestination(line, destinationStart, "line"); + let cursor = parsed.end; + while (/[ \t]/u.test(line[cursor] ?? "")) cursor += 1; + if (cursor < line.length && !/^(?:["'(]).*(?:["')])$/u.test(line.slice(cursor))) { + fail("markdown-link-unsupported", "Markdown reference definition has an unsupported suffix"); + } + const label = normalizeMarkdownLabel(originalLine.slice(indentation + 1, definitionClose)); + if (!label) fail("markdown-link-unsupported", "Markdown reference label is empty"); + if (!definitions.has(label)) definitions.set(label, parsed.target); + } + if (lineEnd < 0) break; + lineStart = lineEnd + 1; + } + + for (let index = 0; index < masked.length; index += 1) { + if (masked[index] !== "[") continue; + const close = closingMarkdownLabel(masked, index + 1); + if (close < 0) fail("markdown-link-unsupported", "Markdown link label is unterminated"); + if (masked[close + 1] === ":") { + index = close; + continue; + } + if (masked[close + 1] === "(") { + if (!normalizeMarkdownLabel(source.slice(index + 1, close))) { + fail("markdown-link-unsupported", "Markdown link label is empty"); + } + const parsed = parseMarkdownDestination(masked, close + 2, ")"); + targets.push(parsed.target); + index = finishInlineMarkdownLink(masked, parsed.end) - 1; + continue; + } + if (masked[close + 1] === "[") { + const referenceEnd = closingMarkdownLabel(masked, close + 2); + if (referenceEnd < 0) fail("markdown-link-unsupported", "Markdown reference label is unterminated"); + const explicit = source.slice(close + 2, referenceEnd); + const label = normalizeMarkdownLabel(explicit || source.slice(index + 1, close)); + if (!label) fail("markdown-link-unsupported", "Markdown reference label is empty"); + if (!definitions.has(label)) fail("markdown-reference-missing", "Markdown reference definition is missing"); + targets.push(definitions.get(label)); + index = referenceEnd; + continue; + } + const shortcut = normalizeMarkdownLabel(source.slice(index + 1, close)); + if (definitions.has(shortcut)) { + targets.push(definitions.get(shortcut)); + index = close; + } + } + return targets; +} + +function localMarkdownTarget(rawTarget, sourceRelative) { + if (typeof rawTarget !== "string" || !rawTarget) return null; + if (rawTarget.startsWith("#") || rawTarget.startsWith("?")) return null; + if (/^[A-Za-z]:[\\/]/u.test(rawTarget) || rawTarget.startsWith("\\\\")) { + fail("markdown-absolute-path", "Markdown closure contains an absolute local path"); + } + const scheme = rawTarget.match(/^([A-Za-z][A-Za-z0-9+.-]*):/u)?.[1]?.toLowerCase(); + if (scheme) { + if (["http", "https", "mailto", "pathname"].includes(scheme)) return null; + if (scheme === "file") fail("markdown-file-uri", "Markdown closure contains a file URI"); + fail("markdown-scheme-invalid", "Markdown closure contains an unsupported URI scheme"); + } + if (rawTarget.startsWith("/") || rawTarget.startsWith("\\") || rawTarget.includes("\\")) { + fail("markdown-absolute-path", "Markdown closure contains an absolute or backslash path"); + } + + const boundary = [rawTarget.indexOf("#"), rawTarget.indexOf("?")] + .filter((index) => index >= 0) + .reduce((minimum, index) => Math.min(minimum, index), rawTarget.length); + const encodedPath = rawTarget.slice(0, boundary); + if (!encodedPath) return null; + let decodedPath; + try { + decodedPath = decodeURIComponent(encodedPath); + } catch { + fail("markdown-percent-invalid", "Markdown closure contains invalid percent encoding"); + } + if (decodedPath.includes("\0") || decodedPath.includes("\\")) { + fail("markdown-path-invalid", "Markdown closure contains an unsafe local path"); + } + const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(sourceRelative), decodedPath)); + if (resolved === ".." || resolved.startsWith("../") || path.posix.isAbsolute(resolved)) { + fail("markdown-path-escape", "Markdown closure target escapes the plugin root"); + } + return resolved; +} + +function markdownTargetCandidates(relativePath) { + return path.posix.extname(relativePath) + ? [relativePath] + : [relativePath, `${relativePath}.md`, `${relativePath}/README.md`]; +} + +async function resolveMarkdownTarget(rawTarget, sourceRelative, candidateExists) { + const unresolved = localMarkdownTarget(rawTarget, sourceRelative); + if (!unresolved) return null; + const candidates = markdownTargetCandidates(unresolved).filter((candidate) => ( + isAllowedArtifactPath(candidate) + )); + if (candidates.length === 0) { + fail("markdown-target-forbidden", "Markdown closure target is outside the artifact allowlist"); + } + const matches = []; + for (const candidate of candidates) { + if (await candidateExists(candidate)) matches.push(candidate); + } + if (matches.length === 0) fail("required-file-missing", "Markdown closure target is missing"); + if (matches.length > 1) fail("markdown-target-ambiguous", "Markdown closure target is ambiguous"); + return matches[0]; +} + +async function traverseMarkdownClosure({ readSource, candidateExists }) { + const queue = [{ relative: CANONICAL_SKILL, depth: 0 }]; + const visited = new Set(); + const targets = new Set(); + let edges = 0; + + while (queue.length) { + const current = queue.shift(); + if (visited.has(current.relative)) continue; + if (visited.size >= GRAPH_LIMITS.markdownNodes) { + fail("markdown-node-limit", "Markdown closure exceeds the fixed node limit"); + } + visited.add(current.relative); + const text = await readSource(current.relative); + for (const rawTarget of parseMarkdownTargets(text)) { + const relative = await resolveMarkdownTarget(rawTarget, current.relative, candidateExists); + if (!relative) continue; + edges += 1; + if (edges > GRAPH_LIMITS.markdownEdges) { + fail("markdown-edge-limit", "Markdown closure exceeds the fixed edge limit"); + } + targets.add(relative); + if (relative.toLowerCase().endsWith(".md") && !visited.has(relative)) { + if (current.depth + 1 > GRAPH_LIMITS.markdownDepth) { + fail("markdown-depth-limit", "Markdown closure exceeds the fixed depth limit"); + } + queue.push({ relative, depth: current.depth + 1 }); + } + } + } + return { + nodes: visited.size, + edges, + files: [...new Set([...visited, ...targets])].sort(), + }; +} + +async function verifyMarkdownClosure(inventory) { + return traverseMarkdownClosure({ + readSource: async (relative) => readBoundedText( + requireRegularFile(inventory, relative), + "markdown-read-failed", + ), + candidateExists: async (relative) => Boolean(inventory.get(relative)?.stats.isFile()), + }); +} + +export async function verifyMarkdownSourceClosure(sourceRoot) { + if (typeof sourceRoot !== "string" || !sourceRoot || sourceRoot.includes("\0")) { + fail("markdown-source-root-invalid", "Markdown source root must be a filesystem path"); + } + const resolvedRoot = path.resolve(sourceRoot); + const canonicalRoot = await realpath(resolvedRoot).catch(() => null); + if (!canonicalRoot) fail("markdown-source-root-invalid", "Markdown source root cannot be resolved"); + + async function regularCandidate(relative, missingIsError = false) { + assertSafeRelative(relative, "markdown-target-forbidden"); + const absolute = path.join(resolvedRoot, ...relative.split("/")); + const stats = await lstat(absolute).catch(() => null); + if (!stats) { + if (missingIsError) fail("required-file-missing", "Markdown closure source is missing"); + return false; + } + if (stats.isSymbolicLink() || !stats.isFile()) { + fail("markdown-source-file-invalid", "Markdown closure source must be a regular non-symlink file"); + } + const canonical = await realpath(absolute).catch(() => null); + if (!canonical || !isContained(canonical, canonicalRoot)) { + fail("markdown-path-escape", "Markdown closure source escapes its canonical root"); + } + return true; + } + + return traverseMarkdownClosure({ + readSource: async (relative) => { + await regularCandidate(relative, true); + const absolute = path.join(resolvedRoot, ...relative.split("/")); + const stats = await lstat(absolute); + return readBoundedText({ absolute, stats }, "markdown-read-failed"); + }, + candidateExists: async (relative) => regularCandidate(relative), + }); +} + +function tokenizeJavaScript(source) { + const tokens = []; + let index = 0; + let canStartRegex = true; + let syntaxBraceDepth = 0; + const parenFrames = []; + const controlHeads = new Set(["catch", "for", "if", "switch", "while", "with"]); + + function push(token) { + tokens.push({ ...token, braceDepth: syntaxBraceDepth }); + if (token.type === "identifier") { + canStartRegex = ["case", "delete", "do", "else", "in", "instanceof", "new", "return", "throw", "typeof", "void", "yield"].includes(token.value); + } else if (["string", "number", "regex"].includes(token.type)) { + canStartRegex = false; + } else if (token.type === "punctuation") { + canStartRegex = ![")", "]", "}"].includes(token.value) && token.value !== "."; + } + } + + function scan(stopAtTemplateBrace = false) { + let braceDepth = 0; + while (index < source.length) { + const char = source[index]; + const next = source[index + 1]; + if (/\s/u.test(char)) { + index += 1; + continue; + } + if (stopAtTemplateBrace && char === "}" && braceDepth === 0) { + syntaxBraceDepth -= 1; + index += 1; + return; + } + if (char === "/" && next === "/") { + index += 2; + while (index < source.length && source[index] !== "\n") index += 1; + continue; + } + if (char === "/" && next === "*") { + const end = source.indexOf("*/", index + 2); + if (end < 0) fail("runtime-comment-invalid", "Runtime module contains an unterminated comment"); + index = end + 2; + continue; + } + if (char === "'" || char === '"') { + const quote = char; + let value = ""; + let escaped = false; + index += 1; + let closed = false; + while (index < source.length) { + if (source[index] === "\\") { + escaped = true; + if (index + 1 >= source.length || /[\r\n]/u.test(source[index + 1])) { + fail("runtime-string-invalid", "Runtime module contains an invalid string escape"); + } + value += source[index + 1]; + index += 2; + continue; + } + if (source[index] === quote) { + closed = true; + index += 1; + break; + } + value += source[index]; + index += 1; + } + if (!closed) fail("runtime-string-invalid", "Runtime module contains an unterminated string"); + push({ type: "string", value, escaped }); + continue; + } + if (char === "`") { + let closed = false; + index += 1; + while (index < source.length) { + if (source[index] === "\\") { + index += 2; + continue; + } + if (source[index] === "$" && source[index + 1] === "{") { + index += 2; + syntaxBraceDepth += 1; + scan(true); + continue; + } + if (source[index] === "`") { + closed = true; + index += 1; + break; + } + index += 1; + } + if (!closed) fail("runtime-template-invalid", "Runtime module contains an unterminated template"); + canStartRegex = false; + continue; + } + if (char === "/" && canStartRegex) { + index += 1; + let inClass = false; + let closed = false; + while (index < source.length) { + if (source[index] === "\\") { + index += 2; + continue; + } + if (source[index] === "[") inClass = true; + else if (source[index] === "]") inClass = false; + else if (source[index] === "/" && !inClass) { + closed = true; + index += 1; + while (/[A-Za-z]/u.test(source[index] ?? "")) index += 1; + break; + } else if (/[\r\n]/u.test(source[index])) { + break; + } + index += 1; + } + if (!closed) fail("runtime-regex-invalid", "Runtime module contains an unterminated regular expression"); + push({ type: "regex", value: "" }); + continue; + } + if (/[A-Za-z_$]/u.test(char)) { + let end = index + 1; + while (end < source.length && /[A-Za-z0-9_$]/u.test(source[end])) end += 1; + push({ type: "identifier", value: source.slice(index, end) }); + index = end; + continue; + } + if (/[0-9]/u.test(char)) { + let end = index + 1; + while (end < source.length && /[A-Za-z0-9_.]/u.test(source[end])) end += 1; + push({ type: "number", value: source.slice(index, end) }); + index = end; + continue; + } + if (char === "(") { + const previous = tokens.at(-1); + const beforePrevious = tokens.at(-2); + parenFrames.push({ + control: ( + previous?.type === "identifier" + && ( + controlHeads.has(previous.value) + || (previous.value === "await" && beforePrevious?.value === "for") + ) + ), + }); + } + if (char === "}" && braceDepth > 0) { + braceDepth -= 1; + syntaxBraceDepth -= 1; + } + push({ type: "punctuation", value: char }); + if (char === "{") { + braceDepth += 1; + syntaxBraceDepth += 1; + } + if (char === ")") { + const frame = parenFrames.pop(); + if (frame?.control) canStartRegex = true; + } + index += 1; + } + if (stopAtTemplateBrace) fail("runtime-template-invalid", "Runtime template expression is unterminated"); + } + + scan(); + return tokens; +} + +function importSpecifier(token, errorCode) { + if (token?.type !== "string") fail(errorCode, "Runtime import/export has a nonliteral specifier"); + if (token.escaped) fail("runtime-import-escape", "Runtime import specifiers must not use string escapes"); + return token.value; +} + +function matchingCloseParen(tokens, openIndex) { + let depth = 0; + for (let index = openIndex; index < tokens.length; index += 1) { + if (tokens[index].value === "(") depth += 1; + if (tokens[index].value === ")") { + depth -= 1; + if (depth === 0) return index; + } + } + return -1; +} + +function isImportMethodName(tokens, importIndex) { + const token = tokens[importIndex]; + if (token.braceDepth <= 0 || tokens[importIndex + 1]?.value !== "(") return false; + const close = matchingCloseParen(tokens, importIndex + 1); + if (close < 0 || tokens[close + 1]?.value !== "{") return false; + return ["{", "}", ";", ","].includes(tokens[importIndex - 1]?.value); +} + +export function parseEsmSpecifiers(source) { + const tokens = tokenizeJavaScript(source); + const specifiers = []; + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index]; + if (token.type !== "identifier" || token.value !== "import") continue; + if (tokens[index - 1]?.value === ".") continue; + if (isImportMethodName(tokens, index)) continue; + const following = tokens[index + 1]; + if (following?.value === ".") continue; + if (following?.value === "(") { + const argument = tokens[index + 2]; + if (argument?.type !== "string" || tokens[index + 3]?.value !== ")") { + fail("runtime-dynamic-import-unresolved", "Runtime contains a nonliteral dynamic import"); + } + specifiers.push(importSpecifier(argument, "runtime-dynamic-import-unresolved")); + continue; + } + if (following?.type === "string") { + specifiers.push(importSpecifier(following, "runtime-import-invalid")); + continue; + } + for (let cursor = index + 1; cursor < tokens.length; cursor += 1) { + if (tokens[cursor].value === ";") break; + if (tokens[cursor].type === "identifier" && tokens[cursor].value === "from") { + specifiers.push(importSpecifier(tokens[cursor + 1], "runtime-import-invalid")); + break; + } + } + } + for (let index = 0; index < tokens.length; index += 1) { + if ( + tokens[index].type !== "identifier" + || tokens[index].value !== "export" + || tokens[index].braceDepth !== 0 + || tokens[index - 1]?.value === "." + ) continue; + for (let cursor = index + 1; cursor < tokens.length; cursor += 1) { + if (tokens[cursor].value === ";") break; + if (tokens[cursor].type === "identifier" && tokens[cursor].value === "from") { + specifiers.push(importSpecifier(tokens[cursor + 1], "runtime-export-invalid")); + break; + } + } + } + return specifiers; +} + +function isAllowedBareDependency(specifier) { + return RUNTIME_DEPENDENCIES.some((dependency) => ( + specifier === dependency || specifier.startsWith(`${dependency}/`) + )); +} + +function splitBareDependency(specifier) { + const dependency = RUNTIME_DEPENDENCIES.find((candidate) => ( + specifier === candidate || specifier.startsWith(`${candidate}/`) + )); + if (!dependency) return null; + return { dependency, subpath: specifier.slice(dependency.length + 1) }; +} + +function dependencySubpathTarget(inventory, dependency, subpath) { + if (!subpath) return null; + if (subpath.includes("\\") || subpath.includes("\0")) { + fail("runtime-dependency-subpath-invalid", "Runtime dependency subpath is unsafe"); + } + let decoded; + try { + decoded = decodeURIComponent(subpath); + } catch { + fail("runtime-dependency-subpath-invalid", "Runtime dependency subpath has invalid encoding"); + } + const normalized = path.posix.normalize(decoded); + if ( + normalized !== decoded + || normalized === "." + || normalized === ".." + || normalized.startsWith("../") + || path.posix.isAbsolute(normalized) + ) { + fail("runtime-dependency-subpath-invalid", "Runtime dependency subpath escapes its package root"); + } + const base = `node_modules/${dependency}/${normalized}`; + const extension = path.posix.extname(base); + const candidates = extension + ? [base] + : [base, `${base}.mjs`, `${base}.js`, `${base}.json`, `${base}/index.mjs`, `${base}/index.js`, `${base}/index.json`]; + const matches = candidates.filter((candidate) => inventory.get(candidate)?.stats.isFile()); + if (matches.length === 0) fail("runtime-dependency-subpath-missing", "Runtime dependency subpath is missing"); + if (matches.length > 1) fail("runtime-dependency-subpath-ambiguous", "Runtime dependency subpath is ambiguous"); + assertSafeRelative(matches[0], "runtime-dependency-subpath-invalid"); + return matches[0]; +} + +function assertAllowedRuntimeRelative(relativePath) { + const normalized = path.posix.normalize(relativePath); + const first = normalized.split("/", 1)[0]; + if ( + normalized !== relativePath + || normalized === ".." + || normalized.startsWith("../") + || path.posix.isAbsolute(normalized) + || !RUNTIME_ROOTS.has(first) + || normalized === "scripts/packaging" + || normalized.startsWith("scripts/packaging/") + ) { + fail("runtime-target-forbidden", "Runtime module target is outside the allowed runtime roots"); + } +} + +function resolveRuntimeTarget(inventory, sourceRelative, rawSpecifier) { + if (rawSpecifier.startsWith("node:")) return { kind: "builtin" }; + if (!rawSpecifier.startsWith("./") && !rawSpecifier.startsWith("../")) { + if (!isAllowedBareDependency(rawSpecifier)) { + fail("runtime-dependency-forbidden", "Runtime imports an unexpected bare dependency"); + } + const parsed = splitBareDependency(rawSpecifier); + const subpathTarget = dependencySubpathTarget(inventory, parsed.dependency, parsed.subpath); + return { kind: "external", dependency: parsed.dependency, subpathTarget }; + } + if (rawSpecifier.includes("\\") || rawSpecifier.includes("\0")) { + fail("runtime-specifier-invalid", "Runtime import contains an unsafe path"); + } + const boundary = [rawSpecifier.indexOf("#"), rawSpecifier.indexOf("?")] + .filter((index) => index >= 0) + .reduce((minimum, index) => Math.min(minimum, index), rawSpecifier.length); + let decoded; + try { + decoded = decodeURIComponent(rawSpecifier.slice(0, boundary)); + } catch { + fail("runtime-percent-invalid", "Runtime import contains invalid percent encoding"); + } + const base = path.posix.normalize(path.posix.join(path.posix.dirname(sourceRelative), decoded)); + assertAllowedRuntimeRelative(base); + const extension = path.posix.extname(base); + const candidates = extension + ? [base] + : [base, `${base}.mjs`, `${base}.js`, `${base}.json`, `${base}/index.mjs`, `${base}/index.js`, `${base}/index.json`]; + const matches = candidates.filter((candidate) => inventory.get(candidate)?.stats.isFile()); + if (matches.length === 0) fail("runtime-target-missing", "Runtime import target is missing"); + if (matches.length > 1) fail("runtime-target-ambiguous", "Runtime import target is ambiguous"); + assertAllowedRuntimeRelative(matches[0]); + return { kind: "local", relative: matches[0] }; +} + +async function verifyRuntimeClosure(inventory) { + const queue = [{ relative: RUNTIME_ENTRY, depth: 0 }]; + const visited = new Set(); + const externalDependencies = new Set(); + let edges = 0; + + while (queue.length) { + const current = queue.shift(); + if (visited.has(current.relative)) continue; + if (visited.size >= GRAPH_LIMITS.runtimeModules) { + fail("runtime-node-limit", "Runtime closure exceeds the fixed module limit"); + } + visited.add(current.relative); + const entry = requireRegularFile(inventory, current.relative); + const extension = path.posix.extname(current.relative).toLowerCase(); + if (extension === ".json") continue; + if (![".mjs", ".js"].includes(extension)) { + fail("runtime-module-type-invalid", "Runtime closure contains an unsupported module type"); + } + const source = await readBoundedText(entry, "runtime-read-failed"); + for (const specifier of parseEsmSpecifiers(source)) { + edges += 1; + if (edges > GRAPH_LIMITS.runtimeEdges) { + fail("runtime-edge-limit", "Runtime closure exceeds the fixed edge limit"); + } + const target = resolveRuntimeTarget(inventory, current.relative, specifier); + if (target.kind === "external") externalDependencies.add(target.dependency); + if (target.kind === "local" && !visited.has(target.relative)) { + if (current.depth + 1 > GRAPH_LIMITS.runtimeDepth) { + fail("runtime-depth-limit", "Runtime closure exceeds the fixed depth limit"); + } + queue.push({ relative: target.relative, depth: current.depth + 1 }); + } + } + } + return { + modules: visited.size, + edges, + files: [...visited].sort(), + externalDependencies: [...externalDependencies].sort(), + }; +} + +export async function verifyAntigravityPluginArtifact(pluginRoot) { + try { + if (typeof pluginRoot !== "string" || !pluginRoot || pluginRoot.includes("\0")) { + fail("plugin-root-invalid", "Plugin root must be a nonempty filesystem path"); + } + const resolvedRoot = path.resolve(pluginRoot); + let rootStats; + try { + rootStats = await lstat(resolvedRoot); + } catch { + fail("plugin-root-missing", "Plugin root is missing or unreadable"); + } + if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) { + fail("plugin-root-type-invalid", "Plugin root must be a real directory"); + } + let canonicalRoot; + try { + canonicalRoot = await realpath(resolvedRoot); + } catch { + fail("plugin-root-unresolvable", "Plugin root cannot be canonically resolved"); + } + const artifactDirectoryName = path.basename(canonicalRoot); + if (artifactDirectoryName !== ANTIGRAVITY_PLUGIN_NAME) { + fail("plugin-root-name-invalid", "Plugin root directory must be named better-harness"); + } + + const tree = await inspectArtifactTree(resolvedRoot, canonicalRoot); + for (const requiredPath of REQUIRED_FILES) requireRegularFile(tree.inventory, requiredPath); + + const manifest = await readJsonObject(tree.inventory, "plugin.json", "manifest-invalid"); + const marker = await readJsonObject( + tree.inventory, + ANTIGRAVITY_ARTIFACT_MARKER, + "marker-invalid", + ); + const packageJson = await readJsonObject(tree.inventory, "package.json", "package-invalid"); + validateMarker(marker); + const pluginName = effectivePluginName(manifest); + if (pluginName !== ANTIGRAVITY_PLUGIN_NAME || marker.pluginName !== pluginName) { + fail("plugin-identity-mismatch", "Plugin root, manifest, and ownership marker identities do not match"); + } + validatePackage(packageJson, marker); + await validateDependencyPackages(tree.inventory, packageJson); + + const markdown = await verifyMarkdownClosure(tree.inventory); + const runtime = await verifyRuntimeClosure(tree.inventory); + return { + kind: ANTIGRAVITY_ARTIFACT_KIND, + schemaVersion: ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + host: ANTIGRAVITY_HOST, + pluginName, + version: marker.version, + canonicalSkill: CANONICAL_SKILL, + runtimeDependencies: [...RUNTIME_DEPENDENCIES], + entryCount: tree.entryCount, + fileCount: tree.fileCount, + totalBytes: tree.totalBytes, + markdownClosure: markdown, + runtimeClosure: runtime, + }; + } catch (error) { + if (error instanceof AntigravityArtifactError) throw error; + fail("artifact-verification-failed", "Artifact verification failed at a protected read boundary"); + } +} + +export function parseArgs(argv) { + const options = { pluginRoot: null, json: false, help: false }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--plugin-root") { + if (options.pluginRoot !== null) fail("argument-duplicate", "--plugin-root may be provided only once"); + const value = argv[index + 1]; + if (!value || value.startsWith("--")) fail("argument-value-missing", "--plugin-root requires a value"); + options.pluginRoot = value; + index += 1; + } else if (arg.startsWith("--plugin-root=")) { + if (options.pluginRoot !== null) fail("argument-duplicate", "--plugin-root may be provided only once"); + const value = arg.slice("--plugin-root=".length); + if (!value) fail("argument-value-missing", "--plugin-root requires a value"); + options.pluginRoot = value; + } else if (arg === "--json") { + options.json = true; + } else if (arg === "--help" || arg === "-h") { + options.help = true; + } else { + fail("argument-unknown", "Unknown argument"); + } + } + if (!options.help && options.pluginRoot === null) { + fail("argument-required", "--plugin-root is required"); + } + return options; +} + +export function usage() { + return [ + "Usage: node scripts/packaging/antigravity/verify-antigravity-plugin.mjs --plugin-root [options]", + "", + "Options:", + " --plugin-root Generated Antigravity plugin root (required)", + " --json Emit parser-safe JSON", + " -h, --help Print help", + "", + ].join("\n"); +} + +export async function main(argv = process.argv.slice(2)) { + let options; + try { + options = parseArgs(argv); + if (options.help) { + process.stdout.write(usage()); + return 0; + } + const result = await verifyAntigravityPluginArtifact(options.pluginRoot); + process.stdout.write(options.json + ? `${JSON.stringify({ ok: true, data: result }, null, 2)}\n` + : `Antigravity plugin artifact verified: ${result.pluginName} ${result.version} (${result.fileCount} files)\n`); + return 0; + } catch (error) { + const code = error instanceof AntigravityArtifactError + ? error.code + : "artifact-verification-failed"; + const message = error instanceof AntigravityArtifactError + ? error.message + : "Artifact verification failed"; + if (options?.json || argv.includes("--json")) { + process.stdout.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}\n`); + } else { + process.stderr.write(`Antigravity plugin verification failed [${code}]: ${message}\n`); + } + return 1; + } +} + +const currentFile = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) { + process.exitCode = await main(); +} diff --git a/test/plugins/antigravity-plugin-artifact.test.mjs b/test/plugins/antigravity-plugin-artifact.test.mjs new file mode 100644 index 00000000..c1428b54 --- /dev/null +++ b/test/plugins/antigravity-plugin-artifact.test.mjs @@ -0,0 +1,1500 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + cp, + lstat, + mkdir, + mkdtemp, + readFile, + realpath, + readdir, + rename, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { test } from "vitest"; + +import { + artifactPackage, + assertOutputBoundary, + buildAntigravityPluginArtifact, + formatBuildSuccess, + publishStagedArtifact, +} from "../../scripts/packaging/antigravity/build-antigravity-plugin.mjs"; + +import { + ANTIGRAVITY_ARTIFACT_KIND, + ANTIGRAVITY_ARTIFACT_MARKER, + ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + ANTIGRAVITY_HOST, + CANONICAL_SKILL, + FILE_LIMITS, + GRAPH_LIMITS, + RUNTIME_DEPENDENCIES, + parseEsmSpecifiers, + parseMarkdownTargets, + validatePortablePathComponent, + validateTraversalBounds, + verifyAntigravityPluginArtifact, + verifyMarkdownSourceClosure, +} from "../../scripts/packaging/antigravity/verify-antigravity-plugin.mjs"; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = path.resolve(currentDir, "../.."); +const verifierPath = path.resolve( + currentDir, + "../../scripts/packaging/antigravity/verify-antigravity-plugin.mjs", +); +const builderPath = path.resolve( + currentDir, + "../../scripts/packaging/antigravity/build-antigravity-plugin.mjs", +); + +async function writeArtifactFile(root, relativePath, content = "fixture\n") { + const target = path.join(root, ...relativePath.split("/")); + await mkdir(path.dirname(target), { recursive: true }); + await writeFile(target, content, "utf8"); +} + +function baseMarker(overrides = {}) { + return { + kind: ANTIGRAVITY_ARTIFACT_KIND, + schemaVersion: ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + host: ANTIGRAVITY_HOST, + pluginName: "better-harness", + version: "1.2.3", + canonicalSkill: CANONICAL_SKILL, + runtimeDependencies: [...RUNTIME_DEPENDENCIES], + ...overrides, + }; +} + +function basePackage(overrides = {}) { + return { + name: "@qoder-ai/better-harness", + version: "1.2.3", + private: true, + license: "MIT", + type: "module", + bin: { "better-harness": "scripts/better-harness.mjs" }, + engines: { node: ">=22", npm: ">=10" }, + dependencies: { + "@vscode/tree-sitter-wasm": "1.0.0", + "esbuild-wasm": "1.0.0", + }, + ...overrides, + }; +} + +function baseSourcePackage(overrides = {}) { + return { + name: "@qoder-ai/better-harness", + version: "1.2.3", + license: "MIT", + type: "module", + bin: { "better-harness": "scripts/better-harness.mjs" }, + engines: { node: ">=22", npm: ">=10" }, + dependencies: { + "@vscode/tree-sitter-wasm": "1.0.0", + "esbuild-wasm": "1.0.0", + }, + ...overrides, + }; +} + +async function createArtifact({ + rootName = "better-harness", + manifest = { name: "better-harness" }, + manifestText, + marker = baseMarker(), + packageJson = basePackage(), +} = {}) { + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-test-")); + const pluginRoot = path.join(container, rootName); + await mkdir(pluginRoot); + for (const relativePath of [ + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + ]) { + await writeArtifactFile(pluginRoot, relativePath); + } + await writeArtifactFile( + pluginRoot, + "plugin.json", + manifestText ?? `${JSON.stringify(manifest)}\n`, + ); + await writeArtifactFile( + pluginRoot, + ANTIGRAVITY_ARTIFACT_MARKER, + `${JSON.stringify(marker)}\n`, + ); + await writeArtifactFile(pluginRoot, "package.json", `${JSON.stringify(packageJson)}\n`); + await writeArtifactFile( + pluginRoot, + CANONICAL_SKILL, + [ + "# Better Harness", + "", + "[Direct](../../references/direct.md)", + "[Guide][guide]", + "[guide]: ../../docs/guide.md#start", + "", + ].join("\n"), + ); + await writeArtifactFile( + pluginRoot, + "references/direct.md", + "[Transitive](../templates/transitive.md?mode=test)\n", + ); + await writeArtifactFile( + pluginRoot, + "templates/transitive.md", + "[Cycle](../skills/better-harness/SKILL.md)\n", + ); + await writeArtifactFile(pluginRoot, "docs/guide.md", "# Guide\n"); + await writeArtifactFile( + pluginRoot, + "scripts/better-harness.mjs", + [ + 'import "./runtime/main.mjs";', + 'export { exported } from "./runtime/exported.mjs";', + 'export const dynamic = import("./runtime/dynamic.mjs");', + "", + ].join("\n"), + ); + await writeArtifactFile( + pluginRoot, + "scripts/runtime/main.mjs", + [ + 'import "node:path";', + 'import "@vscode/tree-sitter-wasm";', + 'export * from "./helper.mjs";', + "", + ].join("\n"), + ); + await writeArtifactFile(pluginRoot, "scripts/runtime/helper.mjs", "export const helper = true;\n"); + await writeArtifactFile(pluginRoot, "scripts/runtime/exported.mjs", "export const exported = true;\n"); + await writeArtifactFile(pluginRoot, "scripts/runtime/dynamic.mjs", "export default true;\n"); + await writeArtifactFile( + pluginRoot, + "node_modules/@vscode/tree-sitter-wasm/package.json", + '{"name":"@vscode/tree-sitter-wasm","version":"1.0.0"}\n', + ); + await writeArtifactFile(pluginRoot, "node_modules/@vscode/tree-sitter-wasm/LICENSE"); + await writeArtifactFile( + pluginRoot, + "node_modules/esbuild-wasm/package.json", + '{"name":"esbuild-wasm","version":"1.0.0"}\n', + ); + await writeArtifactFile(pluginRoot, "node_modules/esbuild-wasm/worker.mjs", "export default true;\n"); + await writeArtifactFile(pluginRoot, "node_modules/esbuild-wasm/LICENSE.md"); + return { container, pluginRoot }; +} + +async function withArtifact(options, callback) { + const artifact = await createArtifact(options); + try { + return await callback(artifact); + } finally { + await rm(artifact.container, { recursive: true, force: true }); + } +} + +async function createPublishFixture() { + const { container, pluginRoot: outputRoot } = await createArtifact(); + const stageRoot = path.join(container, "stage-container", "better-harness"); + await cp(outputRoot, stageRoot, { recursive: true }); + return { container, outputRoot, stageRoot }; +} + +async function createSourceRepo({ manifestText, packageJson } = {}) { + const repoRoot = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-source-")); + for (const relative of [ + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + ]) await writeArtifactFile(repoRoot, relative); + const sourcePackage = packageJson ?? baseSourcePackage(); + await writeArtifactFile(repoRoot, "package.json", `${JSON.stringify(sourcePackage)}\n`); + await writeArtifactFile( + repoRoot, + "scripts/packaging/antigravity/plugin-manifest.json", + manifestText ?? '{"name":"better-harness"}\n', + ); + await writeArtifactFile(repoRoot, CANONICAL_SKILL, "# Better Harness\n"); + await writeArtifactFile(repoRoot, "scripts/better-harness.mjs", "export const fixture = true;\n"); + for (const dependency of RUNTIME_DEPENDENCIES) { + await writeArtifactFile( + repoRoot, + `node_modules/${dependency}/package.json`, + `${JSON.stringify({ name: dependency, version: "1.0.0" })}\n`, + ); + const license = dependency === "esbuild-wasm" ? "LICENSE.md" : "LICENSE"; + await writeArtifactFile(repoRoot, `node_modules/${dependency}/${license}`); + } + return repoRoot; +} + +async function expectCode(promise, code) { + await assert.rejects(promise, (error) => { + assert.equal(error.code, code); + assert.equal(error.message.includes(path.parse(os.tmpdir()).root), false); + return true; + }); +} + +test("verifies the closed Agy CLI manifest variants with recursive Markdown and runtime closure", async () => { + for (const manifest of [ + { name: "better-harness" }, + { name: "better-harness", description: "" }, + { name: "better-harness", description: "Better Harness" }, + ]) { + await withArtifact({ manifest }, async ({ pluginRoot }) => { + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.equal(result.pluginName, "better-harness"); + assert.equal(result.version, "1.2.3"); + assert.deepEqual(result.runtimeDependencies, RUNTIME_DEPENDENCIES); + assert.ok(result.markdownClosure.files.includes("references/direct.md")); + assert.ok(result.markdownClosure.files.includes("templates/transitive.md")); + assert.ok(result.markdownClosure.nodes < 10, "cycle must remain bounded"); + assert.ok(result.runtimeClosure.files.includes("scripts/runtime/helper.mjs")); + assert.ok(result.runtimeClosure.files.includes("scripts/runtime/dynamic.mjs")); + assert.equal(JSON.stringify(result).includes(pluginRoot), false); + }); + } +}); + +test("parses only syntax-authoritative ESM dependencies", () => { + const source = String.raw` + import "./side-effect.mjs"; + import value from "./value.mjs"; + export { value } from "./exported.mjs"; + const ordinary = "escaped\\nstring import('ignored-one')"; + const pattern = /import\("ignored-two"\)/gu; + const object = { import() {} }; + object.import("ignored-three"); + const exporter = { export() { const from = 1; return from; } }; + exporter.export(from); + const meta = import.meta.url; + if (ok) /import("ignored-six")/.test(value); + while (ok) /import("ignored-seven")/.test(value); + do /import("ignored-eight")/.test(value); while (ok); + for await (const item of items) /import("ignored-nine")/.test(item); + // import("ignored-four") + /* export * from "ignored-five" */ + const template = ` + "`text ${import(\"./template-literal.mjs\")} ${ordinary}`" + `; + `; + assert.deepEqual(parseEsmSpecifiers(source), [ + "./side-effect.mjs", + "./value.mjs", + "./template-literal.mjs", + "./exported.mjs", + ]); + assert.throws( + () => parseEsmSpecifiers("const value = `x ${import(target)}`;"), + (error) => error.code === "runtime-dynamic-import-unresolved", + ); + assert.deepEqual(parseEsmSpecifiers([ + "const object = { import(value = import('method-default')) { return import('method-body'); } };", + "class Example { import(value) { return value; } }", + ].join("\n")), ["method-default", "method-body"]); + assert.throws( + () => parseEsmSpecifiers("const value = import(target);"), + (error) => error.code === "runtime-dynamic-import-unresolved", + ); +}); + +test("verifies the pinned real runtime closure through better-harness-cli", async () => { + await withArtifact({}, async ({ pluginRoot }) => { + await rm(path.join(pluginRoot, "scripts"), { recursive: true, force: true }); + await cp(path.join(repositoryRoot, "scripts"), path.join(pluginRoot, "scripts"), { + recursive: true, + filter: (source) => !source.includes(`${path.sep}scripts${path.sep}packaging`), + }); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.ok(result.runtimeClosure.files.includes("scripts/better-harness.mjs")); + assert.ok(result.runtimeClosure.files.includes("scripts/better-harness-cli/cli.mjs")); + assert.equal(result.runtimeClosure.modules, 19); + }); +}); + +test("parses Markdown destinations without treating code as closure", () => { + const targets = parseMarkdownTargets([ + "[angle](<../../docs/my file.md>)", + "[balanced](../../docs/a_(b).md)", + String.raw`[escaped](../../docs/a_\(b\).md)`, + "[reference][guide]", + "[collapsed][]", + "[shortcut]", + "[outer [inner]](../../docs/nested.md)", + String.raw`[outer \[escaped\]](../../docs/escaped.md)`, + "[`inline-code-label`](../../docs/code-label.md)", + "[multi", + " line](../../docs/multiline.md)", + "[guide]: ../../docs/reference.md", + "[collapsed]: ../../docs/collapsed.md", + "[shortcut]: ../../docs/shortcut.md", + "`[inline](../../docs/ignored-inline.md)`", + "```md", + "[fenced](../../docs/ignored-fenced.md)", + "```", + " [indented](../../docs/ignored-indented.md)", + "- item", + " [list-active](../../docs/list-active.md)", + " - nested", + " [nested-active](../../docs/nested-active.md)", + " [nested-code](../../docs/ignored-nested-code.md)", + "", + "[dedented](../../docs/dedented.md)", + ].join("\n")); + assert.deepEqual(targets, [ + "../../docs/my file.md", + "../../docs/a_(b).md", + "../../docs/a_(b).md", + "../../docs/reference.md", + "../../docs/collapsed.md", + "../../docs/shortcut.md", + "../../docs/nested.md", + "../../docs/escaped.md", + "../../docs/code-label.md", + "../../docs/multiline.md", + "../../docs/list-active.md", + "../../docs/nested-active.md", + "../../docs/dedented.md", + ]); + for (const malformed of [ + "[x]( parseMarkdownTargets(malformed), + (error) => error.code === "markdown-link-unsupported", + ); + } +}); + +test("resolves extensionless Markdown targets uniquely and classifies schemes", async () => { + for (const scenario of [ + { target: "../../docs/exact", file: "docs/exact" }, + { target: "../../docs/candidate", file: "docs/candidate.md" }, + { target: "../../docs/guide-dir", file: "docs/guide-dir/README.md" }, + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, `[target](${scenario.target})\n`); + await writeArtifactFile(pluginRoot, scenario.file, "fixture\n"); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.ok(result.markdownClosure.files.includes(scenario.file)); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, "[target](../../docs/ambiguous)\n"); + await writeArtifactFile(pluginRoot, "docs/ambiguous", "fixture\n"); + await writeArtifactFile(pluginRoot, "docs/ambiguous.md", "fixture\n"); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "markdown-target-ambiguous"); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, "[target](../../docs/extensionless-missing)\n"); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, [ + "[pathname](pathname:///demo/adapter-matrix)", + "[http](http://example.com)", + "[https](https://example.com)", + "[mail](mailto:test@example.com)", + "", + ].join("\n")); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.equal(result.markdownClosure.edges, 0); + }); + for (const target of ["file:///private.md", "custom://route"]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, `[target](${target})\n`); + await expectCode( + verifyAntigravityPluginArtifact(pluginRoot), + target.startsWith("file:") ? "markdown-file-uri" : "markdown-scheme-invalid", + ); + }); + } +}); + +test("freezes the pinned canonical Markdown closure and source link classification", async () => { + const closure = await verifyMarkdownSourceClosure(repositoryRoot); + assert.deepEqual( + { nodes: closure.nodes, edges: closure.edges, files: closure.files.length }, + { nodes: 93, edges: 290, files: 96 }, + ); + for (const required of [ + "AGENTS.md", + "DESIGN.md", + "docs/adrs/checkpoint-backed-compare-sources.md", + "docs/adrs/harness-checkpoint-experiment-compare.md", + "docs/docs/hosts/adapter-matrix.md", + "docs/docs/hosts/contributing-new-coding-agent.md", + "references/agent-customize/platforms/qoder.md", + "references/agent-customize/agents-md-review.md", + "references/agent-customize/custom-agents-review.md", + "references/project-harness/observability.md", + ]) assert.ok(closure.files.includes(required), `missing frozen closure file: ${required}`); + assert.equal(closure.files.includes("references/agent-customize/platforms/codex.md"), false); + assert.equal(closure.files.some((file) => file.startsWith(".agents/") || file.startsWith(".github/")), false); + + const sourceAssertions = [ + ["references/project-harness/observability.md", "(../agent-customize/agents-md-review.md)"], + ["references/agent-customize/platforms/qoder.md", "(../custom-agents-review.md)"], + ["references/agent-customize/platforms/codex.md", "(../custom-agents-review.md)"], + ["docs/adapters/contributing-new-coding-agent.md", "https://github.com/QoderAI/better-harness/blob/main/.agents/skills/change-traceability-review/SKILL.md"], + ["docs/adapters/contributing-new-coding-agent.md", "https://github.com/QoderAI/better-harness/blob/main/.github/pull_request_template.md"], + ]; + for (const [relative, expected] of sourceAssertions) { + assert.ok((await readFile(path.join(repositoryRoot, relative), "utf8")).includes(expected)); + } +}); + +test("uses the first duplicate Markdown reference definition", async () => { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, [ + "[reference][duplicate]", + "[duplicate]: ../../../outside.md", + "[duplicate]: ../../docs/guide.md", + "", + ].join("\n")); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "markdown-path-escape"); + }); +}); + +test("rejects invalid manifest identities and shapes", async () => { + const cases = [ + { options: { rootName: "wrong-root", manifest: {} }, code: "plugin-root-name-invalid" }, + { options: { manifest: {} }, code: "manifest-name-required" }, + { options: { manifest: { description: "missing name" } }, code: "manifest-name-required" }, + { options: { manifest: { name: "" } }, code: "manifest-name-blank" }, + { options: { manifest: { name: 7 } }, code: "manifest-name-type-invalid" }, + { options: { manifest: { name: "better harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better/harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better.harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better:harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better\\harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better[harness]" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "other" } }, code: "plugin-identity-mismatch" }, + { options: { manifest: { name: "better-harness", description: null } }, code: "manifest-description-invalid" }, + { options: { manifest: { name: "better-harness", description: 7 } }, code: "manifest-description-invalid" }, + { options: { manifest: { name: "better-harness", unexpected: true } }, code: "manifest-schema-invalid" }, + { + options: { manifest: { name: "better-harness", $schema: "https://example.invalid/schema.json" } }, + code: "manifest-schema-invalid", + }, + { options: { manifestText: "null\n" }, code: "manifest-invalid" }, + { options: { manifestText: "[]\n" }, code: "manifest-invalid" }, + { options: { manifestText: "{\n" }, code: "manifest-invalid" }, + ]; + for (const scenario of cases) { + await withArtifact(scenario.options, async ({ pluginRoot }) => { + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } +}); + +test("rejects missing, escaped, forbidden, and oversized Markdown closure targets", async () => { + const cases = [ + { content: "[Missing](../../references/missing.md)\n", code: "required-file-missing" }, + { content: "[Escape](../../../outside.md)\n", code: "markdown-path-escape" }, + { content: "[Absolute](C:/private/file.md)\n", code: "markdown-absolute-path" }, + { content: "[File](file:///private/file.md)\n", code: "markdown-file-uri" }, + { content: "[Encoded](../../%2e%2e/outside.md)\n", code: "markdown-path-escape" }, + { content: "[Forbidden](../../scripts/packaging/private.md)\n", code: "markdown-target-forbidden" }, + ]; + for (const scenario of cases) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, scenario.content); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + const links = []; + for (let index = 0; index <= GRAPH_LIMITS.markdownNodes; index += 1) { + const relative = `docs/limit-${index}.md`; + links.push(`[${index}](../../${relative})`); + await writeArtifactFile(pluginRoot, relative, `# ${index}\n`); + } + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, `${links.join("\n")}\n`); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "markdown-node-limit"); + }); +}); + +test("rejects unsafe runtime closure imports", async () => { + const cases = [ + { source: 'import "./missing.mjs";\n', code: "runtime-target-missing" }, + { source: 'import "./packaging/private.mjs";\n', code: "runtime-target-forbidden" }, + { source: 'import "unexpected-package";\n', code: "runtime-dependency-forbidden" }, + { source: "const target = './runtime/main.mjs'; import(target);\n", code: "runtime-dynamic-import-unresolved" }, + ]; + for (const scenario of cases) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", scenario.source); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } +}); + +test("enforces every closed ownership marker field", async () => { + const withoutCanonicalSkill = baseMarker(); + delete withoutCanonicalSkill.canonicalSkill; + const cases = [ + { marker: withoutCanonicalSkill, code: "marker-schema-invalid" }, + { marker: { ...baseMarker(), extra: true }, code: "marker-schema-invalid" }, + { marker: baseMarker({ kind: "other" }), code: "marker-kind-invalid" }, + { marker: baseMarker({ schemaVersion: 2 }), code: "marker-schema-version-invalid" }, + { marker: baseMarker({ host: "other" }), code: "marker-host-invalid" }, + { marker: baseMarker({ pluginName: "other" }), code: "plugin-identity-mismatch" }, + { marker: baseMarker({ pluginName: 7 }), code: "marker-plugin-name-invalid" }, + { marker: baseMarker({ version: "" }), code: "marker-version-invalid" }, + { marker: baseMarker({ version: "9.9.9" }), code: "package-version-invalid" }, + { marker: baseMarker({ canonicalSkill: "skills/other/SKILL.md" }), code: "marker-skill-invalid" }, + { marker: baseMarker({ runtimeDependencies: [...RUNTIME_DEPENDENCIES].reverse() }), code: "marker-dependencies-invalid" }, + { marker: baseMarker({ runtimeDependencies: "invalid" }), code: "marker-dependencies-invalid" }, + ]; + for (const scenario of cases) { + await withArtifact({ marker: scenario.marker }, async ({ pluginRoot }) => { + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } +}); + +test("enforces the closed Better Harness artifact package schema", async () => { + const withoutName = basePackage(); + delete withoutName.name; + const withoutBin = basePackage(); + delete withoutBin.bin; + const cases = [ + { packageJson: { ...basePackage(), extra: true }, code: "package-schema-invalid" }, + { packageJson: withoutName, code: "package-schema-invalid" }, + { packageJson: withoutBin, code: "package-schema-invalid" }, + { packageJson: basePackage({ name: "other" }), code: "package-name-invalid" }, + { packageJson: basePackage({ private: false }), code: "package-private-invalid" }, + { packageJson: basePackage({ version: "" }), code: "package-version-invalid" }, + { packageJson: basePackage({ version: "9.9.9" }), code: "package-version-invalid" }, + { packageJson: basePackage({ license: " " }), code: "package-license-invalid" }, + { packageJson: basePackage({ type: "commonjs" }), code: "package-type-invalid" }, + { packageJson: basePackage({ bin: null }), code: "package-bin-invalid" }, + { packageJson: basePackage({ bin: [] }), code: "package-bin-invalid" }, + { packageJson: basePackage({ bin: {} }), code: "package-bin-invalid" }, + { packageJson: basePackage({ bin: { "better-harness": "wrong.mjs" } }), code: "package-bin-invalid" }, + { + packageJson: basePackage({ + bin: { "better-harness": "scripts/better-harness.mjs", extra: "wrong.mjs" }, + }), + code: "package-bin-invalid", + }, + { packageJson: basePackage({ engines: null }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: [] }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: { node: ">=22" } }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: { node: ">=22", npm: "" } }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: { node: 22, npm: ">=10" } }), code: "package-engines-invalid" }, + { + packageJson: basePackage({ engines: { node: ">=22", npm: ">=10", extra: "1" } }), + code: "package-engines-invalid", + }, + { packageJson: basePackage({ dependencies: null }), code: "package-dependencies-invalid" }, + { + packageJson: basePackage({ + dependencies: { ...basePackage().dependencies, unexpected: "1.0.0" }, + }), + code: "package-dependencies-invalid", + }, + { + packageJson: basePackage({ dependencies: { "esbuild-wasm": "1.0.0" } }), + code: "package-dependencies-invalid", + }, + { + packageJson: basePackage({ + dependencies: { + "@vscode/tree-sitter-wasm": "", + "esbuild-wasm": "1.0.0", + }, + }), + code: "package-dependency-version-invalid", + }, + { + packageJson: basePackage({ + dependencies: { + "@vscode/tree-sitter-wasm": 1, + "esbuild-wasm": "1.0.0", + }, + }), + code: "package-dependency-version-invalid", + }, + ]; + for (const scenario of cases) { + await withArtifact({ packageJson: scenario.packageJson }, async ({ pluginRoot }) => { + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } + + for (const packageText of ["null\n", "[]\n", "{\n"]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "package.json", packageText); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "package-invalid"); + }); + } +}); + +test("projects a fresh exact artifact package and rejects malformed source runtime metadata", () => { + const source = baseSourcePackage({ description: "allowed source-only metadata" }); + const projected = artifactPackage(source); + assert.deepEqual(Object.keys(projected).sort(), [ + "bin", + "dependencies", + "engines", + "license", + "name", + "private", + "type", + "version", + ]); + assert.deepEqual(projected, basePackage()); + + source.bin["better-harness"] = "mutated.mjs"; + source.engines.node = "mutated"; + source.dependencies["esbuild-wasm"] = "mutated"; + assert.equal(projected.bin["better-harness"], "scripts/better-harness.mjs"); + assert.equal(projected.engines.node, ">=22"); + assert.equal(projected.dependencies["esbuild-wasm"], "1.0.0"); + + const cases = [ + { value: baseSourcePackage({ name: "wrong" }), code: "source-package-invalid" }, + { value: baseSourcePackage({ type: "commonjs" }), code: "source-package-invalid" }, + { value: baseSourcePackage({ bin: null }), code: "source-bin-invalid" }, + { value: baseSourcePackage({ bin: [] }), code: "source-bin-invalid" }, + { value: baseSourcePackage({ bin: {} }), code: "source-bin-invalid" }, + { value: baseSourcePackage({ bin: { "better-harness": "wrong.mjs" } }), code: "source-bin-invalid" }, + { + value: baseSourcePackage({ + bin: { "better-harness": "scripts/better-harness.mjs", extra: "wrong.mjs" }, + }), + code: "source-bin-invalid", + }, + { value: baseSourcePackage({ engines: null }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: [] }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: { node: ">=22" } }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: { node: ">=22", npm: " " } }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: { node: 22, npm: ">=10" } }), code: "source-engines-invalid" }, + { + value: baseSourcePackage({ engines: { node: ">=22", npm: ">=10", extra: "1" } }), + code: "source-engines-invalid", + }, + ]; + for (const scenario of cases) { + assert.throws(() => artifactPackage(scenario.value), (error) => error.code === scenario.code); + } +}); + +test("binds dependency package identity, version, and imported subpaths", async () => { + const metadataCases = [ + { relative: "node_modules/esbuild-wasm/package.json", text: "{\n", code: "dependency-package-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: "null\n", code: "dependency-package-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: '{"name":"wrong","version":"1.0.0"}\n', code: "dependency-package-name-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: '{"name":"esbuild-wasm"}\n', code: "dependency-package-version-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: '{"name":"esbuild-wasm","version":"2.0.0"}\n', code: "dependency-package-version-invalid" }, + { relative: "node_modules/@vscode/tree-sitter-wasm/package.json", text: '{"name":"wrong","version":"1.0.0"}\n', code: "dependency-package-name-invalid" }, + ]; + for (const scenario of metadataCases) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, scenario.relative, scenario.text); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", 'import "esbuild-wasm/worker";\n'); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.deepEqual(result.runtimeClosure.externalDependencies, ["esbuild-wasm"]); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", 'import "esbuild-wasm/missing";\n'); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "runtime-dependency-subpath-missing"); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "node_modules/esbuild-wasm/worker.js", "export default true;\n"); + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", 'import "esbuild-wasm/worker";\n'); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "runtime-dependency-subpath-ambiguous"); + }); +}); + +test("enforces host-neutral portable components and traversal bounds", () => { + for (const component of [ + "CON", + "con.txt", + "PrN.json", + "COM1", + "lpt9.log", + "trailing.", + "trailing ", + "stream:name", + "back\\slash", + `control${String.fromCharCode(1)}`, + "x".repeat(FILE_LIMITS.componentLength + 1), + ]) { + assert.throws( + () => validatePortablePathComponent(component), + (error) => error.code === "path-component-invalid", + ); + } + assert.equal(validatePortablePathComponent("portable-name.md"), "portable-name.md"); + assert.throws( + () => validateTraversalBounds(FILE_LIMITS.entries + 1, 0), + (error) => error.code === "artifact-entry-limit", + ); + assert.throws( + () => validateTraversalBounds(0, FILE_LIMITS.directoryDepth + 1), + (error) => error.code === "artifact-depth-limit", + ); +}); + +test("rejects unknown, host, development, Skill, and packaging tree entries", async () => { + for (const relativePath of [ + "unknown.txt", + ".codex-plugin/plugin.json", + "skills/other/SKILL.md", + "scripts/packaging/private.mjs", + "test/fixture.txt", + "docs/cache/output.txt", + "docs/private.log", + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, relativePath); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "path-not-allowed"); + }); + } +}); + +test("requires the complete artifact and dependency license profile", async () => { + for (const relativePath of [ + "LICENSE", + CANONICAL_SKILL, + "scripts/better-harness.mjs", + "node_modules/@vscode/tree-sitter-wasm/package.json", + "node_modules/@vscode/tree-sitter-wasm/LICENSE", + "node_modules/esbuild-wasm/LICENSE.md", + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await rm(path.join(pluginRoot, ...relativePath.split("/"))); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + const license = path.join(pluginRoot, "LICENSE"); + await rm(license); + await mkdir(license); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); +}); + +test("rejects a missing transitive Markdown target", async () => { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "references/direct.md", "[Missing](../docs/missing.md)\n"); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); +}); + +test("rejects case-insensitive or NFC path identity collisions where constructable", async (context) => { + let exercised = false; + for (const pair of [ + ["docs/Case.md", "docs/case.md"], + ["docs/caf\u00e9.md", "docs/cafe\u0301.md"], + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, pair[0]); + await writeArtifactFile(pluginRoot, pair[1]); + const names = await readdir(path.join(pluginRoot, "docs")); + if (!pair.every((relative) => names.includes(path.basename(relative)))) return; + exercised = true; + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "path-identity-collision"); + }); + } + if (!exercised) context.skip("filesystem does not preserve a constructable case or NFC collision"); +}); + +test("rejects special files where a FIFO can be created", async (context) => { + if (process.platform === "win32") { + context.skip("portable FIFO creation is unavailable on Windows"); + return; + } + await withArtifact({}, async ({ pluginRoot }) => { + const fifo = path.join(pluginRoot, "docs", "fixture.fifo"); + const created = spawnSync("mkfifo", [fifo], { encoding: "utf8" }); + if (created.status !== 0) { + context.skip("mkfifo is unavailable in this environment"); + return; + } + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "special-file-forbidden"); + }); +}); + +test("CLI help and JSON envelopes are parser-safe and imports have no side effects", async () => { + const imported = spawnSync( + process.execPath, + ["--input-type=module", "--eval", `await import(${JSON.stringify(pathToFileURL(verifierPath).href)})`], + { encoding: "utf8" }, + ); + assert.equal(imported.status, 0, imported.stderr); + assert.equal(imported.stdout, ""); + assert.equal(imported.stderr, ""); + + const help = spawnSync(process.execPath, [verifierPath, "--help"], { encoding: "utf8" }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /--plugin-root /u); + + for (const args of [["--json"], ["--json", "--unknown"], ["--json", "--plugin-root"]]) { + const failure = spawnSync(process.execPath, [verifierPath, ...args], { encoding: "utf8" }); + assert.equal(failure.status, 1); + assert.equal(failure.stderr, ""); + const envelope = JSON.parse(failure.stdout); + assert.equal(envelope.ok, false); + assert.equal(typeof envelope.error.code, "string"); + } + + const duplicate = spawnSync( + process.execPath, + [verifierPath, "--json", "--plugin-root", "one", "--plugin-root", "two"], + { encoding: "utf8" }, + ); + assert.equal(duplicate.status, 1); + assert.equal(JSON.parse(duplicate.stdout).error.code, "argument-duplicate"); + + await withArtifact({}, async ({ pluginRoot }) => { + const success = spawnSync( + process.execPath, + [verifierPath, "--plugin-root", pluginRoot, "--json"], + { encoding: "utf8" }, + ); + assert.equal(success.status, 0, success.stderr); + const envelope = JSON.parse(success.stdout); + assert.equal(envelope.ok, true); + assert.equal(envelope.data.pluginName, "better-harness"); + assert.equal(success.stdout.includes(pluginRoot), false); + + const missingRoot = path.join(pluginRoot, "private", "missing"); + const failure = spawnSync( + process.execPath, + [verifierPath, `--plugin-root=${missingRoot}`, "--json"], + { encoding: "utf8" }, + ); + assert.equal(failure.status, 1); + assert.equal(failure.stdout.includes(missingRoot), false); + assert.equal(JSON.parse(failure.stdout).error.code, "plugin-root-missing"); + }); +}); + +test("rejects symbolic links when the platform permits creating one", async (context) => { + await withArtifact({}, async ({ pluginRoot }) => { + const target = path.join(pluginRoot, "docs", "guide.md"); + const link = path.join(pluginRoot, "docs", "linked.md"); + try { + await symlink(target, link, "file"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`symlink creation unavailable: ${error.code}`); + return; + } + throw error; + } + assert.equal((await lstat(link)).isSymbolicLink(), true); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "symlink-forbidden"); + }); +}); + +test("builds, verifies, runs, and atomically replaces the real pinned artifact", async () => { + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-build-")); + const outputRoot = path.join(container, "better-harness"); + try { + const first = await buildAntigravityPluginArtifact({ repoRoot: repositoryRoot, outputRoot }); + assert.equal(first.replaced, false); + assert.deepEqual(first.publication, { + state: "published", + backupCleanup: "complete", + replaced: false, + }); + assert.deepEqual(first.warnings, []); + assert.deepEqual( + JSON.parse(await readFile(path.join(outputRoot, "plugin.json"), "utf8")), + JSON.parse(await readFile(path.join(repositoryRoot, "scripts/packaging/antigravity/plugin-manifest.json"), "utf8")), + ); + for (const relative of [CANONICAL_SKILL, "DESIGN.md", "references/project-harness/observability.md"]) { + assert.deepEqual( + await readFile(path.join(outputRoot, ...relative.split("/"))), + await readFile(path.join(repositoryRoot, ...relative.split("/"))), + ); + } + assert.deepEqual(await readdir(path.join(outputRoot, "skills")), ["better-harness"]); + assert.deepEqual( + Object.keys(JSON.parse(await readFile(path.join(outputRoot, "package.json"), "utf8"))).sort(), + ["bin", "dependencies", "engines", "license", "name", "private", "type", "version"], + ); + for (const relative of [".agents", ".github", "test", "scripts/packaging"]) { + assert.equal(await lstat(path.join(outputRoot, ...relative.split("/"))).catch(() => null), null); + } + const verified = await verifyAntigravityPluginArtifact(outputRoot); + assert.deepEqual( + { nodes: verified.markdownClosure.nodes, edges: verified.markdownClosure.edges, files: verified.markdownClosure.files.length }, + { nodes: 93, edges: 290, files: 96 }, + ); + assert.equal(verified.runtimeClosure.modules, 19); + const help = spawnSync(process.execPath, ["scripts/better-harness.mjs", "--help"], { + cwd: outputRoot, + encoding: "utf8", + }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /Better Harness CLI/u); + + await writeArtifactFile(outputRoot, "docs/old-sentinel.md", "old\n"); + const second = await buildAntigravityPluginArtifact({ repoRoot: repositoryRoot, outputRoot }); + assert.equal(second.replaced, true); + assert.deepEqual(second.publication, { + state: "published", + backupCleanup: "complete", + replaced: true, + }); + assert.equal(await lstat(path.join(outputRoot, "docs/old-sentinel.md")).catch(() => null), null); + await verifyAntigravityPluginArtifact(outputRoot); + assert.equal( + (await readdir(container)).some((name) => name.includes("stage-") || name.includes("backup-")), + false, + ); + } finally { + await rm(container, { recursive: true, force: true }); + } +}, 30_000); + +test("preserves unowned and full-invalid destinations before copying", async () => { + const repoRoot = await createSourceRepo(); + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-owned-")); + try { + for (const kind of ["unowned", "marker-only"]) { + const parent = path.join(container, kind); + const outputRoot = path.join(parent, "better-harness"); + await writeArtifactFile(outputRoot, "sentinel.txt", `${kind}\n`); + if (kind === "marker-only") { + await writeArtifactFile( + outputRoot, + ANTIGRAVITY_ARTIFACT_MARKER, + `${JSON.stringify(baseMarker())}\n`, + ); + } + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot }), + "destination-unowned", + ); + assert.equal(await readFile(path.join(outputRoot, "sentinel.txt"), "utf8"), `${kind}\n`); + } + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(container, { recursive: true, force: true }); + } +}); + +test("fails closed on source metadata, dependency, and staged closure errors", async () => { + const cases = [ + { manifestText: '{"name":"better-harness","extra":true}\n', code: "source-manifest-invalid" }, + { manifestText: "{\n", code: "source-manifest-invalid" }, + { + packageJson: baseSourcePackage({ + dependencies: { + "@vscode/tree-sitter-wasm": "1.0.0", + "esbuild-wasm": "1.0.0", + unexpected: "1.0.0", + }, + }), + code: "source-dependencies-invalid", + }, + ]; + for (const scenario of cases) { + const repoRoot = await createSourceRepo(scenario); + const parent = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-invalid-")); + try { + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + scenario.code, + ); + assert.deepEqual(await readdir(parent), []); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(parent, { recursive: true, force: true }); + } + } + + const repoRoot = await createSourceRepo(); + const parent = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-stage-invalid-")); + try { + await rm(path.join(repoRoot, "node_modules/esbuild-wasm/LICENSE.md")); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "dependency-license-invalid", + ); + assert.deepEqual(await readdir(parent), []); + await writeArtifactFile(repoRoot, "node_modules/esbuild-wasm/LICENSE.md"); + await writeArtifactFile(repoRoot, CANONICAL_SKILL, "[missing](../../references/missing.md)\n"); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "required-file-missing", + ); + assert.deepEqual(await readdir(parent), []); + await writeArtifactFile(repoRoot, CANONICAL_SKILL, "# Better Harness\n"); + await rm(path.join(repoRoot, "README.md")); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "source-entry-unreadable", + ); + assert.deepEqual(await readdir(parent), []); + await writeArtifactFile(repoRoot, "README.md"); + await rm(path.join(repoRoot, "node_modules/esbuild-wasm/package.json")); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "dependency-package-invalid", + ); + assert.deepEqual(await readdir(parent), []); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(parent, { recursive: true, force: true }); + } +}); + +test("rejects source and output-parent symlinks when the platform permits them", async (context) => { + const repoRoot = await createSourceRepo(); + const parent = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-symlink-")); + try { + const sourceTarget = path.join(repoRoot, "docs-target.md"); + const sourceLink = path.join(repoRoot, "docs", "linked.md"); + await writeArtifactFile(repoRoot, "docs-target.md"); + await mkdir(path.dirname(sourceLink), { recursive: true }); + try { + await symlink(sourceTarget, sourceLink, "file"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`source/output symlink creation unavailable: ${error.code}`); + return; + } + throw error; + } + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "source-symlink-forbidden", + ); + + await rm(sourceLink); + const linkedParent = path.join(parent, "linked-parent"); + await symlink(repoRoot, linkedParent, "junction"); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(linkedParent, "better-harness") }), + "output-overlap", + ); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(parent, { recursive: true, force: true }); + } +}); + +test("plans output boundaries without writes and rechecks canonical authority after parent creation", async () => { + const repoRoot = await createSourceRepo(); + const external = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-boundary-")); + try { + const rejectedParent = path.join(repoRoot, "missing", "deep"); + await expectCode( + assertOutputBoundary(repoRoot, path.join(rejectedParent, "better-harness")), + "output-overlap", + ); + assert.equal(await lstat(path.join(repoRoot, "missing")).catch(() => null), null); + + const approvedParent = path.join(external, "approved", "deep"); + const approvedOutput = path.join(approvedParent, "better-harness"); + const preflight = await assertOutputBoundary(repoRoot, approvedOutput); + assert.equal(await lstat(path.join(external, "approved")).catch(() => null), null); + await mkdir(preflight.resolvedParent, { recursive: true }); + const postCreate = await assertOutputBoundary(repoRoot, approvedOutput); + assert.equal(postCreate.canonicalParent, await realpath(approvedParent)); + assert.equal(postCreate.canonicalOutput, path.join(await realpath(approvedParent), "better-harness")); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(external, { recursive: true, force: true }); + } +}); + +test("rejects a parent junction retargeted into the repository between boundary checks", async (context) => { + const repoRoot = await createSourceRepo(); + const external = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-retarget-")); + const approvedTarget = path.join(external, "approved-target"); + const linkedParent = path.join(external, "linked-parent"); + await mkdir(approvedTarget); + try { + try { + await symlink(approvedTarget, linkedParent, "junction"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`junction retarget unavailable: ${error.code}`); + return; + } + throw error; + } + const outputRoot = path.join(linkedParent, "deep", "better-harness"); + const preflight = await assertOutputBoundary(repoRoot, outputRoot); + await mkdir(preflight.canonicalParent, { recursive: true }); + await rm(linkedParent); + await symlink(repoRoot, linkedParent, "junction"); + assert.ok(await lstat(path.join(approvedTarget, "deep"))); + assert.equal(await lstat(path.join(repoRoot, "deep")).catch(() => null), null); + await expectCode(assertOutputBoundary(repoRoot, outputRoot), "output-overlap"); + assert.equal(await lstat(path.join(repoRoot, "deep")).catch(() => null), null); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(external, { recursive: true, force: true }); + } +}); + +test("pins boundary mkdir to the safe canonical parent across a pre-mkdir junction retarget", async (context) => { + const repoRoot = await createSourceRepo(); + const external = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-mkdir-race-")); + const approvedTarget = path.join(external, "approved-target"); + const linkedParent = path.join(external, "linked-parent"); + await mkdir(approvedTarget); + try { + try { + await symlink(approvedTarget, linkedParent, "junction"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`junction retarget unavailable: ${error.code}`); + return; + } + throw error; + } + const outputRoot = path.join(linkedParent, "deep", "better-harness"); + const expectedCanonicalParent = path.join(await realpath(approvedTarget), "deep"); + let mkdirCalls = 0; + let publicationCalls = 0; + await expectCode( + buildAntigravityPluginArtifact({ + repoRoot, + outputRoot, + boundaryOperations: { + mkdir: async (target, options) => { + mkdirCalls += 1; + assert.equal(target, expectedCanonicalParent); + assert.notEqual(target, path.dirname(outputRoot)); + await rm(linkedParent); + await symlink(repoRoot, linkedParent, "junction"); + await mkdir(target, options); + }, + }, + operations: { + rename: async () => { publicationCalls += 1; }, + remove: async () => { publicationCalls += 1; }, + }, + }), + "output-overlap", + ); + assert.equal(mkdirCalls, 1); + assert.equal(publicationCalls, 0); + assert.ok(await lstat(expectedCanonicalParent)); + assert.equal(await lstat(path.join(repoRoot, "deep")).catch(() => null), null); + assert.equal(await lstat(path.join(repoRoot, "deep", "better-harness")).catch(() => null), null); + assert.equal( + (await readdir(approvedTarget)).some((name) => name.includes("stage-") || name.includes("backup-")), + false, + ); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(external, { recursive: true, force: true }); + } +}); + +test("publishes only after full backup verification and restores verified backup on publish failure", async () => { + for (const rollbackFails of [false, true]) { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + let renameCalls = 0; + const operations = { + rename: async (source, destination) => { + renameCalls += 1; + if (renameCalls === 2 || (rollbackFails && renameCalls === 3)) throw new Error("injected"); + await rename(source, destination); + }, + remove: rm, + }; + try { + await expectCode( + publishStagedArtifact({ stageRoot, outputRoot, replaceExisting: true, operations }), + rollbackFails ? "publish-rollback-failed" : "publish-failed", + ); + if (rollbackFails) { + assert.equal(await lstat(outputRoot).catch(() => null), null); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, 1); + await verifyAntigravityPluginArtifact(path.join(container, backups[0], "better-harness")); + } else { + await verifyAntigravityPluginArtifact(outputRoot); + assert.equal((await readdir(container)).some((name) => name.includes("backup-")), false); + } + assert.ok(await lstat(stageRoot)); + } finally { + await rm(container, { recursive: true, force: true }); + } + } +}); + +test("reports a stable first-backup-rename failure and bounds empty-container cleanup", async () => { + for (const cleanupFails of [false, true]) { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + let removeCalls = 0; + try { + await assert.rejects( + publishStagedArtifact({ + stageRoot, + outputRoot, + replaceExisting: true, + operations: { + rename: async () => { throw new Error("injected first rename failure"); }, + remove: async (...args) => { + removeCalls += 1; + if (cleanupFails) throw new Error("injected cleanup failure"); + await rm(...args); + }, + }, + }), + (error) => { + assert.equal(error.code, "publish-backup-rename-failed"); + assert.equal(error.publication.state, "not-published-destination-unchanged"); + assert.equal(error.publication.backupCleanup, cleanupFails ? "pending" : "complete"); + assert.equal(JSON.stringify(error).includes(container), false); + return true; + }, + ); + assert.equal(removeCalls, 1); + await verifyAntigravityPluginArtifact(outputRoot); + await verifyAntigravityPluginArtifact(stageRoot); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, cleanupFails ? 1 : 0); + } finally { + await rm(container, { recursive: true, force: true }); + } + } +}); + +test("revalidates the actual moved destination and restores concurrent unowned or corrupt swaps", async () => { + for (const swapKind of ["unowned", "corrupt"] ) { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + const originalRoot = path.join(container, "original-container", "better-harness"); + const swappedRoot = path.join(container, "concurrent-swap"); + try { + await mkdir(path.dirname(originalRoot)); + await rename(outputRoot, originalRoot); + if (swapKind === "unowned") { + await writeArtifactFile(swappedRoot, "concurrent.txt", "preserve me\n"); + } else { + await cp(originalRoot, swappedRoot, { recursive: true }); + await writeArtifactFile(swappedRoot, ANTIGRAVITY_ARTIFACT_MARKER, "{}\n"); + await writeArtifactFile(swappedRoot, "docs/concurrent.md", "preserve me\n"); + } + await rename(swappedRoot, outputRoot); + + await assert.rejects( + publishStagedArtifact({ stageRoot, outputRoot, replaceExisting: true }), + (error) => { + assert.equal(error.code, "destination-changed"); + assert.equal(error.publication.state, "not-published-destination-restored"); + return true; + }, + ); + const sentinel = swapKind === "unowned" ? "concurrent.txt" : "docs/concurrent.md"; + assert.equal(await readFile(path.join(outputRoot, ...sentinel.split("/")), "utf8"), "preserve me\n"); + await verifyAntigravityPluginArtifact(originalRoot); + await verifyAntigravityPluginArtifact(stageRoot); + assert.equal((await readdir(container)).some((name) => name.includes("backup-")), false); + } finally { + await rm(container, { recursive: true, force: true }); + } + } +}); + +test("retains the moved concurrent tree when destination revalidation rollback conflicts", async () => { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + const originalRoot = path.join(container, "original-container", "better-harness"); + const swappedRoot = path.join(container, "concurrent-swap"); + await mkdir(path.dirname(originalRoot)); + await rename(outputRoot, originalRoot); + await writeArtifactFile(swappedRoot, "concurrent.txt", "preserve me\n"); + await rename(swappedRoot, outputRoot); + let renameCalls = 0; + try { + await assert.rejects( + publishStagedArtifact({ + stageRoot, + outputRoot, + replaceExisting: true, + operations: { + rename: async (source, destination) => { + renameCalls += 1; + if (renameCalls === 2) { + await writeArtifactFile(outputRoot, "conflict.txt", "also preserve me\n"); + throw new Error("injected rollback conflict"); + } + await rename(source, destination); + }, + remove: rm, + }, + }), + (error) => { + assert.equal(error.code, "destination-revalidation-rollback-failed"); + assert.equal(error.publication.state, "not-published-backup-retained"); + assert.equal(JSON.stringify(error).includes(container), false); + return true; + }, + ); + assert.equal(await readFile(path.join(outputRoot, "conflict.txt"), "utf8"), "also preserve me\n"); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, 1); + assert.equal( + await readFile(path.join(container, backups[0], "better-harness", "concurrent.txt"), "utf8"), + "preserve me\n", + ); + await verifyAntigravityPluginArtifact(stageRoot); + await verifyAntigravityPluginArtifact(originalRoot); + } finally { + await rm(container, { recursive: true, force: true }); + } +}); + +test("restores a concurrent destination symlink without dereferencing it when supported", async (context) => { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + const originalRoot = path.join(container, "original-container", "better-harness"); + const targetRoot = path.join(container, "concurrent-target"); + try { + await mkdir(path.dirname(originalRoot)); + await rename(outputRoot, originalRoot); + await writeArtifactFile(targetRoot, "sentinel.txt", "preserve target\n"); + try { + await symlink(targetRoot, outputRoot, "junction"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`destination symlink creation unavailable: ${error.code}`); + return; + } + throw error; + } + await expectCode( + publishStagedArtifact({ stageRoot, outputRoot, replaceExisting: true }), + "destination-changed", + ); + assert.equal((await lstat(outputRoot)).isSymbolicLink(), true); + assert.equal(await readFile(path.join(targetRoot, "sentinel.txt"), "utf8"), "preserve target\n"); + await verifyAntigravityPluginArtifact(stageRoot); + await verifyAntigravityPluginArtifact(originalRoot); + } finally { + await rm(container, { recursive: true, force: true }); + } +}); + +test("returns published cleanup-pending state and renders safe JSON and human warnings", async () => { + const repoRoot = await createSourceRepo(); + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-cleanup-")); + const outputRoot = path.join(container, "better-harness"); + try { + await buildAntigravityPluginArtifact({ repoRoot, outputRoot }); + await writeArtifactFile(repoRoot, "README.md", "new artifact\n"); + let removeCalls = 0; + let renameCalls = 0; + const result = await buildAntigravityPluginArtifact({ + repoRoot, + outputRoot, + operations: { + rename: async (source, destination) => { + renameCalls += 1; + await rename(source, destination); + }, + remove: async () => { + removeCalls += 1; + throw new Error("injected backup cleanup failure"); + }, + }, + }); + assert.equal(renameCalls, 2); + assert.equal(removeCalls, 1); + assert.equal(result.publication.state, "published"); + assert.equal(result.publication.backupCleanup, "pending"); + assert.equal(result.warnings.length, 1); + assert.equal(result.warnings[0].code, "backup-cleanup-pending"); + assert.equal(await readFile(path.join(outputRoot, "README.md"), "utf8"), "new artifact\n"); + await verifyAntigravityPluginArtifact(outputRoot); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, 1); + assert.equal( + await readFile(path.join(container, backups[0], "better-harness", "README.md"), "utf8"), + "fixture\n", + ); + + const human = formatBuildSuccess(result); + assert.match(human, /backup-cleanup-pending/u); + assert.match(human, /state=published backupCleanup=pending/u); + assert.equal(human.includes(container), false); + const jsonText = formatBuildSuccess(result, { json: true }); + const json = JSON.parse(jsonText); + assert.equal(json.ok, true); + assert.equal(json.data.publication.state, "published"); + assert.equal(json.warnings[0].code, "backup-cleanup-pending"); + assert.equal(jsonText.includes(container), false); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(container, { recursive: true, force: true }); + } +}); + +test("builder CLI and output boundaries fail closed without path disclosure", async () => { + const imported = spawnSync( + process.execPath, + ["--input-type=module", "--eval", `await import(${JSON.stringify(pathToFileURL(builderPath).href)})`], + { encoding: "utf8" }, + ); + assert.equal(imported.status, 0, imported.stderr); + assert.equal(imported.stdout, ""); + assert.equal(imported.stderr, ""); + const help = spawnSync(process.execPath, [builderPath, "--help"], { encoding: "utf8" }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /--out /u); + for (const args of [["--json"], ["--json", "--unknown"], ["--json", "--out"], ["--json", "--out", "one", "--out", "two"]]) { + const result = spawnSync(process.execPath, [builderPath, ...args], { encoding: "utf8" }); + assert.equal(result.status, 1); + assert.equal(result.stderr, ""); + assert.equal(JSON.parse(result.stdout).ok, false); + } + + const privatePath = path.join(os.tmpdir(), "private-builder-fixture", "wrong-name"); + const failure = spawnSync( + process.execPath, + [builderPath, "--json", "--out", privatePath], + { encoding: "utf8" }, + ); + assert.equal(failure.status, 1); + assert.equal(failure.stdout.includes(privatePath), false); + assert.equal(JSON.parse(failure.stdout).error.code, "output-invalid"); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot: repositoryRoot, outputRoot: path.join(repositoryRoot, "docs", "better-harness") }), + "output-overlap", + ); +}); From 986f23017179e8ad1420e079d1736f1ff748712e Mon Sep 17 00:00:00 2001 From: aaaazqh <258566949+aaaazqh@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:41:36 +0800 Subject: [PATCH 2/3] test: make artifact privacy assertion portable --- .../antigravity-plugin-artifact.test.mjs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/plugins/antigravity-plugin-artifact.test.mjs b/test/plugins/antigravity-plugin-artifact.test.mjs index c1428b54..7675d0ef 100644 --- a/test/plugins/antigravity-plugin-artifact.test.mjs +++ b/test/plugins/antigravity-plugin-artifact.test.mjs @@ -249,11 +249,25 @@ async function createSourceRepo({ manifestText, packageJson } = {}) { } async function expectCode(promise, code) { - await assert.rejects(promise, (error) => { - assert.equal(error.code, code); - assert.equal(error.message.includes(path.parse(os.tmpdir()).root), false); - return true; - }); + const outcome = promise.then( + (value) => ({ status: "fulfilled", value }), + (error) => ({ status: "rejected", error }), + ); + const tempPrefixes = [...new Set([path.resolve(os.tmpdir()), await realpath(os.tmpdir())])] + .filter((tempPrefix) => tempPrefix !== path.parse(tempPrefix).root); + await assert.rejects( + outcome.then((result) => { + if (result.status === "rejected") throw result.error; + return result.value; + }), + (error) => { + assert.equal(error.code, code); + for (const tempPrefix of tempPrefixes) { + assert.equal(error.message.includes(tempPrefix), false); + } + return true; + }, + ); } test("verifies the closed Agy CLI manifest variants with recursive Markdown and runtime closure", async () => { From abc0706d96ee25629d2f5c47f397858ed84548be Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Fri, 21 Aug 2026 23:49:15 +0800 Subject: [PATCH 3/3] test(docs): check the link styles the artifact closure relies on The three reference links this branch repairs were broken in the repository already. They survived because `classify` cannot tell a slash-free `custom-agents-review.md`, a real sibling one directory up, from `AGENTS.md`, a file convention in a repository under analysis, so it treats every slash-free token as conceptual and checks neither. Check slash-free targets written as link syntax, excluding the convention names, so the repair cannot silently regress. `docs/adapters/contributing-new-coding-agent.md` cites `.agents/` and `.github/` as repository URLs because the artifact allowlist excludes both. That keeps the link working inside the artifact but gives up the guarantee a relative link had: the doc-link graph no longer notices when the target moves. Check that repository URLs at a mutable ref still resolve, which covers 44 such links across the site tree and this doc rather than only the two here. Name the offending entry when the builder or verifier rejects a path. A dependency release that starts shipping a `dist/` directory fails the build, and `Source path is outside the artifact allowlist` alone leaves no way to tell which of 739 entries caused it. Details carry repository-relative paths only, so errors still cannot leak the operator's filesystem layout. Co-Authored-By: Claude Opus 5 --- ...26-08-19-67-antigravity-plugin-artifact.md | 9 +++ .../antigravity/build-antigravity-plugin.mjs | 17 +++-- .../antigravity/verify-antigravity-plugin.mjs | 17 +++-- test/skills-docs/doc-link-graph.test.mjs | 72 +++++++++++++++++++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md b/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md index 8f81ca66..67f654ac 100644 --- a/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md +++ b/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md @@ -161,6 +161,15 @@ self-contained without changing product behavior: 3. the observability instructions link to the canonical Agent instructions review reference. +Groups 2 and 3 correct links that were already broken in the repository. They +survived because `classify` in `scripts/doc-link-graph/cli.mjs` cannot tell a +slash-free `custom-agents-review.md` (a real sibling one directory up) from +`AGENTS.md` (a file convention in a repository under analysis), so it checks +neither. `test/skills-docs/doc-link-graph.test.mjs` now checks slash-free +targets written as link syntax, excluding the convention names, and checks that +repository URLs at a mutable ref still resolve — the guarantee group 1 gives up +by leaving the artifact boundary. + ## Build, Boundary, and Publication Contract `build-antigravity-plugin.mjs` stages into a unique sibling container, assembles diff --git a/scripts/packaging/antigravity/build-antigravity-plugin.mjs b/scripts/packaging/antigravity/build-antigravity-plugin.mjs index 8c3ce834..92a1494e 100644 --- a/scripts/packaging/antigravity/build-antigravity-plugin.mjs +++ b/scripts/packaging/antigravity/build-antigravity-plugin.mjs @@ -139,18 +139,23 @@ function shouldSkipSource(relativePath) { async function copyRegularTree({ source, destination, relativePath, canonicalBoundary }) { if (shouldSkipSource(relativePath)) return; for (const component of relativePath.split("/")) validatePortablePathComponent(component); - if (!isAllowedArtifactPath(relativePath)) fail("source-path-forbidden", "Source path is outside the artifact allowlist"); + // Name the entry. A dependency release that starts shipping a `dist/` or a + // stray `.log` fails the build here, and an error without the path leaves no + // way to tell which of a few hundred entries caused it. + if (!isAllowedArtifactPath(relativePath)) { + fail("source-path-forbidden", "Source path is outside the artifact allowlist", { path: relativePath }); + } let stats; try { stats = await lstat(source); } catch { - fail("source-entry-unreadable", "Allowlisted source entry is missing or unreadable"); + fail("source-entry-unreadable", "Allowlisted source entry is missing or unreadable", { path: relativePath }); } - if (stats.isSymbolicLink()) fail("source-symlink-forbidden", "Source artifact input must not be a symbolic link"); - if (!stats.isDirectory() && !stats.isFile()) fail("source-special-file-forbidden", "Source artifact input must be regular"); + if (stats.isSymbolicLink()) fail("source-symlink-forbidden", "Source artifact input must not be a symbolic link", { path: relativePath }); + if (!stats.isDirectory() && !stats.isFile()) fail("source-special-file-forbidden", "Source artifact input must be regular", { path: relativePath }); const canonical = await realpath(source).catch(() => null); if (!canonical || !isContained(canonical, canonicalBoundary)) { - fail("source-path-escape", "Source artifact input escapes its canonical root"); + fail("source-path-escape", "Source artifact input escapes its canonical root", { path: relativePath }); } if (stats.isFile()) { await mkdir(path.dirname(destination), { recursive: true }); @@ -159,7 +164,7 @@ async function copyRegularTree({ source, destination, relativePath, canonicalBou } await mkdir(destination, { recursive: true }); const entries = await readdir(source, { withFileTypes: true }).catch(() => { - fail("source-directory-unreadable", "Allowlisted source directory cannot be read"); + fail("source-directory-unreadable", "Allowlisted source directory cannot be read", { path: relativePath }); }); entries.sort((left, right) => left.name.localeCompare(right.name, "en")); for (const entry of entries) { diff --git a/scripts/packaging/antigravity/verify-antigravity-plugin.mjs b/scripts/packaging/antigravity/verify-antigravity-plugin.mjs index ca39f88c..863faf64 100644 --- a/scripts/packaging/antigravity/verify-antigravity-plugin.mjs +++ b/scripts/packaging/antigravity/verify-antigravity-plugin.mjs @@ -126,8 +126,13 @@ export class AntigravityArtifactError extends Error { } } -function fail(code, message) { - throw new AntigravityArtifactError(code, message); +// `details` carries repository- or artifact-relative paths only. An absolute +// path would leak the operator's filesystem layout into an error a caller may +// log, which is why the messages themselves stay location-free. +function fail(code, message, details = {}) { + const error = new AntigravityArtifactError(code, message); + Object.assign(error, details); + throw error; } function isObject(value) { @@ -780,7 +785,11 @@ async function resolveMarkdownTarget(rawTarget, sourceRelative, candidateExists) isAllowedArtifactPath(candidate) )); if (candidates.length === 0) { - fail("markdown-target-forbidden", "Markdown closure target is outside the artifact allowlist"); + fail( + "markdown-target-forbidden", + "Markdown closure target is outside the artifact allowlist", + { source: sourceRelative, target: unresolved }, + ); } const matches = []; for (const candidate of candidates) { @@ -1179,7 +1188,7 @@ function assertAllowedRuntimeRelative(relativePath) { || normalized === "scripts/packaging" || normalized.startsWith("scripts/packaging/") ) { - fail("runtime-target-forbidden", "Runtime module target is outside the allowed runtime roots"); + fail("runtime-target-forbidden", "Runtime module target is outside the allowed runtime roots", { target: relativePath }); } } diff --git a/test/skills-docs/doc-link-graph.test.mjs b/test/skills-docs/doc-link-graph.test.mjs index a17873eb..b3acf8d5 100644 --- a/test/skills-docs/doc-link-graph.test.mjs +++ b/test/skills-docs/doc-link-graph.test.mjs @@ -23,6 +23,35 @@ const DOC_DIRS = [ "schemas", ]; const ROOT_DOCS = ["AGENTS.md", "README.md"]; +// Docs that cannot reach a target with a relative path — the Docusaurus site +// tree, and repository docs that ship inside a host plugin artifact whose +// allowlist excludes `.agents/` and `.github/` — cite it as a repository URL +// instead. Those citations are real links to real files, so they need the same +// integrity guarantee a relative link gets. +const REPO_BLOB_URL = /https:\/\/github\.com\/QoderAI\/better-harness\/blob\/([^/\s)]+)\/([^)\s#]+)/g; +const PINNED_COMMIT = /^[0-9a-f]{40}$/u; +// A bare `foo.md` link usually names a file convention in an analyzed target +// repository rather than a file here, which is why `classify` leaves it +// conceptual. These names are the conventions; anything else that is written as +// link syntax is meant to resolve. +const CONVENTION_DOC_NAMES = new Set([ + "AGENTS.md", + "CHANGELOG.md", + "CLAUDE.local.md", + "CLAUDE.md", + "CODEX.md", + "CONTRIBUTING.md", + "DESIGN.md", + "GEMINI.md", + "MEMORY.md", + "QODER.md", + "README.md", + "SECURITY.md", + "SKILL.md", + "SUPPORT.md", + "report.md", +]); +const MARKDOWN_LINK = /\]\(([^)\s#]+\.md)(?:#[^)]*)?\)/g; const MD_TOKEN = /(?:\.\.?\/)*(?:[\w.-]+\/)*[\w.-]+\.md\b/g; // Bilingual reader surfaces: the Docusaurus site tree (including its zh-Hans // locale mirror and specs that quote Chinese reader copy) and the root README @@ -81,6 +110,49 @@ test("all relative markdown doc links across the repo resolve", () => { ); }); +test("repository URLs pointing at a mutable ref resolve to real files", () => { + // A relative link rots loudly when its target moves, because the link graph + // fails. A repository URL rots silently. Checking the path component keeps + // both citation styles under the same guarantee. Commit-pinned URLs are + // deliberate references to history and stay out of scope. + const broken = []; + for (const file of allRepoDocs()) { + for (const [, ref, target] of readFileSync(file, "utf8").matchAll(REPO_BLOB_URL)) { + if (PINNED_COMMIT.test(ref)) continue; + if (!existsSync(path.join(repoRoot, target))) { + broken.push(`${relId(file)} -> ${ref}/${target}`); + } + } + } + assert.deepEqual( + broken, + [], + `Repository URLs whose path no longer exists:\n${broken.join("\n")}`, + ); +}); + +test("bare filename markdown links resolve unless they name a file convention", () => { + // `classify` cannot tell `custom-agents-review.md` (a real sibling doc one + // directory up) from `AGENTS.md` (a convention in a repository under + // analysis), so it treats every slash-free token as conceptual and checks + // neither. Restricting the check to link syntax and excluding the convention + // names recovers the first case without reintroducing false positives. + const broken = []; + for (const file of allRepoDocs()) { + for (const [, target] of readFileSync(file, "utf8").matchAll(MARKDOWN_LINK)) { + if (target.includes("/") || target.startsWith(".") || CONVENTION_DOC_NAMES.has(target)) continue; + if (!existsSync(path.resolve(path.dirname(file), target))) { + broken.push(`${relId(file)} -> ${target}`); + } + } + } + assert.deepEqual( + broken, + [], + `Bare filename links that do not resolve next to their doc:\n${broken.join("\n")}`, + ); +}); + test("Better Harness skill doc graph has no missing link targets", () => { const seeds = markdownFilesUnder(path.join(repoRoot, "skills/better-harness")); const graph = buildGraph(seeds, { follow: true });