Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/scripts/fallow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ fi

base_branch="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-${TABELLIO_BASE_BRANCH:-main}}"
git fetch --no-tags origin "+refs/heads/${base_branch}:refs/remotes/origin/${base_branch}"
. .buildkite/scripts/security-tools.sh
npm install --global fallow@2.89.0 c8@10.1.3
bash .buildkite/scripts/provenance-coverage.sh

Expand Down
1 change: 1 addition & 0 deletions .buildkite/scripts/product-validation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ if [[ "$pull_request" == "false" && "$build_context" != "preflight" && "$default
fi

. .buildkite/scripts/verify-git-toolchain.sh
. .buildkite/scripts/security-tools.sh

candidate="${BUILDKITE_COMMIT:-HEAD}"
base_branch="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-${TABELLIO_BASE_BRANCH:-main}}"
Expand Down
47 changes: 47 additions & 0 deletions .buildkite/scripts/security-tools.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail

# Source this script to retain PATH in Buildkite. GitHub receives the same paths
# through its runner environment files. No candidate package scripts are run.
if [[ -n "${TABELLIO_SECURITY_TOOLS_DIR:-}" ]]; then
security_tools_root="$TABELLIO_SECURITY_TOOLS_DIR"
else
security_tools_root="$(mktemp -d "${TMPDIR:-/tmp}/tabellio-security-tools.XXXXXX")"
fi
mkdir -p "$security_tools_root"
case "$(uname -s)-$(uname -m)" in
Darwin-arm64)
security_archive_name="gitleaks_8.30.1_darwin_arm64.tar.gz"
security_archive_digest="b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5"
;;
Linux-x86_64)
security_archive_name="gitleaks_8.30.1_linux_x64.tar.gz"
security_archive_digest="551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb"
;;
*) printf '%s\n' 'Unsupported security tool platform.' >&2; exit 1 ;;
esac
security_archive_path="$security_tools_root/$security_archive_name"
if [[ ! -f "$security_archive_path" ]]; then
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
--connect-timeout 15 --max-time 120 \
"https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/$security_archive_name" \
--output "$security_archive_path"
fi
node --input-type=module - "$security_archive_path" "$security_archive_digest" <<'NODE'
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const actual = createHash('sha256').update(readFileSync(process.argv[2])).digest('hex');
if (actual !== process.argv[3]) throw new Error('Security scanner archive integrity mismatch.');
NODE
tar -xzf "$security_archive_path" -C "$security_tools_root" gitleaks
npm install --prefix "$security_tools_root" --no-save --no-package-lock --ignore-scripts \
--registry https://registry.npmjs.org @ast-grep/cli@0.45.1
export PATH="$security_tools_root:$security_tools_root/node_modules/.bin:$PATH"
export TABELLIO_GITLEAKS="$security_tools_root/gitleaks"
export TABELLIO_REQUIRE_SECURITY_SCANNERS=1
test "$(gitleaks version)" = "8.30.1"
test "$(ast-grep --version)" = "ast-grep 0.45.1"
if [[ -n "${GITHUB_PATH:-}" ]]; then
printf '%s\n' "$security_tools_root" "$security_tools_root/node_modules/.bin" >> "$GITHUB_PATH"
printf 'TABELLIO_GITLEAKS=%s\nTABELLIO_REQUIRE_SECURITY_SCANNERS=1\n' "$TABELLIO_GITLEAKS" >> "$GITHUB_ENV"
fi
1 change: 1 addition & 0 deletions .buildkite/scripts/tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
set -euo pipefail

. .buildkite/scripts/verify-git-toolchain.sh
. .buildkite/scripts/security-tools.sh
npm run check
node scripts/write-tabellio-evidence-envelope.mjs --out tabellio-pr-evidence.json
node scripts/check-tabellio-evidence-envelope.mjs --evidence tabellio-pr-evidence.json
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/product-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
test -x "$postgres_bin/initdb"
test -x "$postgres_bin/pg_ctl"
echo "$postgres_bin" >> "$GITHUB_PATH"
- name: Install pinned security scanners
run: bash .buildkite/scripts/security-tools.sh
- name: Resolve squash-merge checkpoint revision
id: checkpoint
if: github.event_name == 'push'
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
branches:
- main
workflow_dispatch:
inputs:
base_branch:
description: Branch to compare for changed-code checks
required: true
default: main
type: string

permissions:
contents: read
Expand All @@ -26,6 +32,8 @@ jobs:
with:
node-version: 20
package-manager-cache: false
- name: Install pinned security scanners
run: bash .buildkite/scripts/security-tools.sh
- name: Run repository checks
run: npm run check

Expand All @@ -43,16 +51,19 @@ jobs:
with:
node-version: 20
package-manager-cache: false
- name: Install pinned security scanners
run: bash .buildkite/scripts/security-tools.sh
- name: Install pinned Fallow
run: npm install --global fallow@2.89.0 c8@10.1.3
- name: Measure provenance function and statement coverage
run: bash .buildkite/scripts/provenance-coverage.sh
- name: Enforce changed-code quality gate
env:
FALLOW_AGENT_SOURCE: codex
FALLOW_BASE_BRANCH: ${{ github.base_ref || inputs.base_branch || 'main' }}
run: |
fallow audit \
--base "origin/${{ github.base_ref }}" \
--base "origin/$FALLOW_BASE_BRANCH" \
--gate new-only \
--health-baseline quality-baselines/fallow-health.json \
--dupes-baseline quality-baselines/fallow-dupes.json \
Expand Down
4 changes: 2 additions & 2 deletions .tabellio/validators.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
"summary": "Agent run and review lifecycle examples"
},
"security": {
"commands": [["node", "scripts/check-tabellio-external-actions.mjs", "--evidence", "examples/tabellio-evidence/minimal-evidence.json"]],
"commands": [["node", "scripts/check-tabellio-external-actions.mjs", "--evidence", "examples/tabellio-evidence/minimal-evidence.json"], ["node", "scripts/check-provenance-security.mjs"]],
"metrics": [{"name": "external_action_policy_pass", "unit": "boolean", "passValue": 1, "failValue": 0}],
"cost": {"telemetry": "available", "usd": 0, "modelCalls": 0, "toolCalls": 0},
"summary": "External-action and side-effect policy checks"
"summary": "External-action policy and real candidate-bound security scanner regressions"
},
"analytics-static": {
"commands": [["node", "--test", "tests/analytics-core.test.mjs"]],
Expand Down
81 changes: 77 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,88 @@ Git and PostgreSQL operations are real; Plane, Entire, GitHub, Buildkite, and
security observations in this sample are explicitly synthetic. This is not a
live-provider or security-scanner certification.

The demo receipt includes a failure matrix with expected and actual verdicts,
safe reasons, and lineage digests where available. It exercises missing, stale,
conflicting, tampered, secret, failed-validation, outage, and moved-base cases.
Source replay runs twice in a newly created local database, verifies the original
digest, and checks that source snapshots and Git refs remain unchanged. Cleanup
and local cost/time are recorded even when the demo fails.

To replace the sample security observation with real bounded checks, install
Gitleaks 8.30.1 and ast-grep 0.45.1 on `PATH`, then run:

```bash
node scripts/demo-provenance.mjs --verify-security-scanners true
```

On Apple Silicon macOS or x86-64 Linux, `. .buildkite/scripts/security-tools.sh`
installs the pinned tools into a temporary directory and exposes them on `PATH`.
Set `TABELLIO_SECURITY_TOOLS_DIR` to reuse a tool directory. The installer verifies
the Gitleaks archive checksum and does not run npm package install scripts.
Configured CI uses this setup before checks; the required security
validator fails when scanners are missing instead of skipping scanner fixtures.
Run the same required check locally with `npm run tabellio:provenance:security:check`.

The scanner reads immutable Git blobs without running candidate code. It uses
Gitleaks defaults and explicit JavaScript/TypeScript rules for unverified JWT
decoding, unsigned JWT configuration, disabled TLS verification, and dynamic
`eval`. Candidate ignore files and suppression comments cannot grant a pass.
Insecure HTTP dependency references fail; other declared npm dependencies remain
blocked pending vulnerability evidence. These checks cover the listed rules;
they do not establish the absence of all authorization or dependency defects.

`tabellio-provenance security --input <lineage.json> --repo <repo> --now <time>`
produces a separate security receipt. `import-security` accepts that receipt with
the scoped lineage query, current repository candidate, and expected
`--policy-digest`. It binds the receipt to the same safe packet and candidate,
rejects modified receipts, and preserves failed or unavailable checks as blocking
evidence. Findings contain rule IDs, severity, file locations, and content
digests; matched secrets and source snippets are omitted. The findings contract
is `schemas/provenance-security-review.schema.json`.

For development validation, include PostgreSQL integration tests in that same
isolated cluster:

```bash
node scripts/demo-provenance.mjs --verify-storage-tests true --out /tmp/tabellio-demo.json
```

The `tabellio-provenance` CLI supports `capture`, `import`, `replay`, `show`,
`review`, and `packet`. Database operations require an explicit local
`review` returns the full current candidate, distinct review and security verdicts,
actions for failed or blocked evidence, and matching GitHub status payloads. Known
Git and provider records receive source links; other records retain their exact
source identifiers and lineage digest. Load verified security finding locations
with `--security-input <receipt.json> --policy-digest <expected-digest>`.
`--report-url` supplies a credential-free report link for both status contexts.

`review-intent` prepares an immutable publication intent from the same scoped
lineage query. `publish-review` accepts `--intent-input` and `--approval-input`,
uses `GH_TOKEN`, rechecks the current candidate and GitHub origin,
and publishes separate `Tabellio / provenance review` and
`Tabellio / provenance security` contexts. The approval uses
`tabellio-provenance-status-approval/v0.1` with `id`, `intentDigest`, `approved: true`,
`approvedBy`, `approvedAt`, `expiresAt`, and `reason`; its lifetime is at most one
hour. Publication receipts report delivery separately from review verdicts.
Each approval is reserved in `refs/tabellio/provenance-statuses` before delivery;
repeated requests reuse the receipt, and uncertain attempts require inspection
before a new approval. The local demo exercises this flow through a fake GitHub
transport and compares the delivered states with the CLI result.

The `tabellio-provenance` CLI supports `capture`, `import`, `import-sources`, `replay`, `replay-sources`,
`show`, `review`, `review-intent`, `publish-review`, and `packet`. `import-sources` normalizes a bundle of Plane,
Entire, GitHub, and Buildkite snapshots and captures Git directly from `--repo`.
Readers preserve healthy sources while reporting authentication, permission,
missing-record, outage, and malformed-input failures as blocked. The demo imports
all five sources, then verifies missing independent security evidence blocks review.
External snapshots remain explicitly synthetic in the sample.
The packet contract is `schemas/provenance-review-packet.schema.json`. Packets
contain only candidate-scoped facts and fixed failure explanations; their complete
JSON envelope, including its digest, is limited to 65,536 UTF-8 bytes.
`replay-sources` rebuilds from the original source bundle using `--repo`, `--input`,
the original capture time in `--now`, and `--expected-digest` from the import receipt.
It writes only when the rebuilt digest matches; changed or missing sources return
blocked evidence without storing a replacement. Reordering snapshots and repeating
the replay preserve the same record. Source snapshots are never modified.
Database operations require an explicit local
`--database-url`; review and packet commands also require `--repo` so readiness
is checked against current Git state. `--now` supplies a deterministic evaluation
time for fixtures; normal operation uses current time. The lower-level
Expand All @@ -71,8 +144,8 @@ never an inherited application's `DATABASE_URL`.

The capture and retention boundary lives in `tabellio.data-boundary.json`.
Raw prompts, transcripts, provider bodies, and credentials are excluded. The
remaining rebuild work includes source adapters, independent security evidence,
GitHub presentation, and the final release decision. No cloud provisioning,
remaining release work includes failure/recovery acceptance and the explicit
release decision. No cloud provisioning,
automatic publication, deployment, or learning is introduced.

The native engine runs through the installed `git` executable. It never constructs shell commands.
Expand Down
36 changes: 36 additions & 0 deletions examples/provenance/sources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFile } from "node:fs/promises";
import { digestObject } from "../../scripts/lib/stack-operation.mjs";

// External provider snapshots are synthetic. Git capture is replaced by the CLI.
export async function sampleSourceBundle(candidate, now) {
const projectId = "11111111-1111-4111-8111-111111111111";
const stateId = "22222222-2222-4222-8222-222222222222";
const selection = { taskId: "33333333-3333-4333-8333-333333333333", taskIdentifier: `${candidate.projectKey}-1`, sessionId: "session-example", checkpointId: "abcdef123456", pullRequestNumber: 1, reviewId: "review-1", organization: "example", pipeline: "tabellio", buildNumber: 1, manifestDigest: "c".repeat(64) };
const example = async (path) => JSON.parse(await readFile(new URL(path, import.meta.url), "utf8"));
const entire = await example("../tabellio-ledger/minimal-ledger.json");
entire.repository.id = candidate.repositoryId;
entire.capturedAt = now;
entire.range = { baseCommit: candidate.baseCommit, headCommit: candidate.headCommit };
entire.checkpoints[0].commits = [candidate.headCommit];
const validation = await example("../tabellio-validation/minimal-result.json");
validation.repository.id = candidate.repositoryId;
validation.revision = { baseCommit: candidate.baseCommit, headCommit: candidate.headCommit, mergeBase: candidate.mergeBase };
validation.checkpointRevision = { ...validation.revision };
validation.suite = { id: "sample", manifestPath: "tabellio.validation.json", manifestDigest: selection.manifestDigest };
validation.runner = { id: "sample", runtime: "node" };
const { integrity, ...unsigned } = validation;
integrity.digest = digestObject(unsigned);
const snapshots = {
plane: { schemaVersion: "tabellio-plane-work-items/v0.1", workspace: "sample", capturedAt: now, status: "available", reason: null,
projects: [{ id: projectId, identifier: candidate.projectKey }], states: [{ id: stateId, projectId, group: "backlog" }],
workItems: [{ id: selection.taskId, projectId, stateId, sequenceNumber: 1, createdAt: now, updatedAt: now, targetDate: null }] },
git: { schemaVersion: "tabellio-git-source/v0.1", capturedAt: now, candidate, taskIdentifier: selection.taskIdentifier, checkpointId: selection.checkpointId },
entire,
github: { repositoryId: candidate.repositoryId, capturedAt: now,
changeRequest: { number: 1, state: "open", draft: false, source: { commit: candidate.headCommit }, target: { commit: candidate.baseCommit } },
reviews: [{ id: selection.reviewId, state: "approved", commit: candidate.headCommit, body: `Tabellio-Candidate: ${candidate.id}` }] },
buildkite: { snapshot: { schemaVersion: "tabellio-buildkite-build-snapshot/v0.1", repository: candidate.repositoryId, organization: "example", pipeline: "tabellio", capturedAt: now, status: "available", reason: null,
builds: [{ number: 1, commit: candidate.headCommit, state: "passed", createdAt: "2026-07-10T12:00:00.000Z", finishedAt: "2026-07-10T12:00:01.000Z", jobCount: 1, artifactCount: 1 }] }, validation },
};
return { candidate, selection, snapshots };
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@
"check": "npm test && npm run tabellio:platform:check && npm run tabellio:run:example:check && npm run tabellio:stack:example:check && npm run tabellio:stack:operation:example:check && npm run tabellio:review:example:check && npm run tabellio:validate:example:check && npm run tabellio:design-memory:example:check && npm run tabellio:ledger:example:check && npm run tabellio:release:example:check && npm run tabellio:context:example:check && npm run tabellio:analytics:product:check && npm run tabellio:evidence:example:check && npm run tabellio:deployment:check && node scripts/check-tabellio-external-actions.mjs --evidence examples/tabellio-evidence/minimal-evidence.json",
"tabellio:local-store": "node scripts/tabellio-local-store.mjs",
"tabellio:provenance": "node scripts/tabellio-provenance.mjs",
"tabellio:provenance:demo": "node scripts/demo-provenance.mjs"
"tabellio:provenance:demo": "node scripts/demo-provenance.mjs",
"tabellio:provenance:security:check": "node scripts/check-provenance-security.mjs"
},
"engines": {
"node": ">=20"
Expand Down
55 changes: 55 additions & 0 deletions schemas/provenance-review-packet.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:tabellio:schema:provenance-review-packet:v0.1",
"title": "Tabellio Provenance Review Packet",
"$comment": "Non-authoritative derived context. The producer enforces a 65536-byte UTF-8 limit including the digest, exact-candidate binding, credential screening, and source-authority rules.",
"type": "object",
"additionalProperties": false,
"required": ["schemaVersion", "authoritative", "candidate", "lineageDigest", "status", "reasons", "facts", "redactions", "digest"],
"properties": {
"schemaVersion": { "const": "tabellio-review-packet/v0.1" },
"authoritative": { "const": false },
"candidate": { "$ref": "#/$defs/candidate" },
"lineageDigest": { "$ref": "#/$defs/digest" },
"digest": { "$ref": "#/$defs/digest" },
"status": { "enum": ["passed", "failed", "blocked"] },
"reasons": { "type": "array", "items": { "$ref": "#/$defs/reason" } },
"facts": { "type": "array", "maxItems": 256, "items": { "$ref": "#/$defs/fact" } },
"redactions": { "type": "array", "minItems": 1, "maxItems": 1, "items": { "const": "Source payloads, raw content, and private checkpoint metadata are omitted." } }
},
"$defs": {
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"oid": { "type": "string", "pattern": "^(?:[a-f0-9]{40}|[a-f0-9]{64})$" },
"text": { "type": "string", "minLength": 1, "maxLength": 512, "pattern": "\\S" },
"candidate": {
"type": "object", "additionalProperties": false,
"required": ["projectKey", "repositoryId", "baseCommit", "headCommit", "mergeBase", "id"],
"properties": {
"projectKey": { "$ref": "#/$defs/text" }, "repositoryId": { "$ref": "#/$defs/text" },
"baseCommit": { "$ref": "#/$defs/oid" }, "headCommit": { "$ref": "#/$defs/oid" },
"mergeBase": { "$ref": "#/$defs/oid" }, "id": { "$ref": "#/$defs/digest" }
}
},
"fact": {
"type": "object", "additionalProperties": false,
"required": ["id", "source", "sourceId", "observedAt", "kind", "status", "candidateId"],
"properties": {
"id": { "$ref": "#/$defs/digest" }, "candidateId": { "$ref": "#/$defs/digest" },
"source": { "enum": ["plane", "git", "entire", "github", "buildkite", "tabellio"] },
"sourceId": { "$ref": "#/$defs/text" }, "observedAt": { "type": "string", "format": "date-time" },
"kind": { "enum": ["task", "run", "commit", "checkpoint", "pull_request", "validation", "review", "security"] },
"status": { "enum": ["present", "passed", "failed", "missing", "stale", "conflicting", "inferred", "blocked"] }
}
},
"reason": {
"type": "object", "additionalProperties": false,
"required": ["state", "kind", "message", "evidenceId"],
"properties": {
"state": { "enum": ["failed", "missing", "stale", "conflicting", "inferred", "blocked"] },
"kind": { "enum": ["candidate", "task", "run", "commit", "checkpoint", "pull_request", "validation", "review", "security"] },
"message": { "$ref": "#/$defs/text" },
"evidenceId": { "oneOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }] }
}
}
}
}
Loading
Loading