Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/ci/candidate-suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ record() {
echo '# candidate protocol suite. It is NOT a published release, NOT a release'
echo '# claim and NOT a conformance claim, and it must not be cited as any of'
echo '# them. The committed default pin is unchanged by this run; promoting it'
echo '# is owned by TASK-260720-38l1sy after TASK-260720-25d05o qualifies the'
echo '# release.'
echo '# is owned solely by TASK-260728-d8ktna after rc.5 release qualification.'
echo '#'
echo "candidate_revision $ref"
echo "candidate_root $root"
Expand All @@ -150,6 +149,7 @@ record() {
echo "tree_sha256 sha256:$tree_sha"
echo "file_count $files"
echo "committed_released_pin ${SPEC_PIN:-<unset>}"
echo 'pin_promotion_owner TASK-260728-d8ktna'
echo "runner_goos $(go env GOOS 2>/dev/null || echo '<unknown>')"
echo "runner_goarch $(go env GOARCH 2>/dev/null || echo '<unknown>')"
echo 'evidence_class candidate-only'
Expand Down
11 changes: 11 additions & 0 deletions .github/ci/gate-selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ PASS=0; FAIL=0; SKIPPED=0
# these cases also assert that the pin CI actually uses is itself a full,
# immutable, lowercase 40-hex revision -- the same shape a candidate must have.
PIN="$(awk '/^[ \t]*SPEC_PIN:[ \t]*/{print $2; exit}' .github/workflows/ci.yml)"
QUALIFIED_PIN='00b1688a9b2457ca397a0bb550acf47cad8ee967'

ok() { PASS=$((PASS + 1)); printf 'ok %s\n' "$1"; }
bad() { FAIL=$((FAIL + 1)); printf 'FAIL %s\n %s\n' "$1" "$2"; }
Expand All @@ -51,6 +52,15 @@ assert_contains() {
fi
}

if [ "$PIN" = "$QUALIFIED_PIN" ]; then
ok 'the default SPEC_PIN remains on the currently qualified release'
else
bad 'the default SPEC_PIN remains on the currently qualified release' "got $PIN, want $QUALIFIED_PIN; TASK-260728-d8ktna owns promotion"
fi
assert_contains 'the candidate revision remains an explicit workflow input' 'ref: ${{ inputs.candidate_ref }}' .github/workflows/ci.yml
assert_contains 'the candidate root remains an explicit workflow input' 'CANDIDATE_ROOT_INPUT: ${{ inputs.candidate_root }}' .github/workflows/ci.yml
assert_contains 'the candidate lane remains non-default' "github.event_name == 'workflow_dispatch'" .github/workflows/ci.yml

echo '=== candidate-suite.sh verify-ref: only a full immutable revision is a candidate ==='
CS="$HERE/candidate-suite.sh"
assert 'candidate inputs reject revision plus root' 1 bash "$CS" verify-inputs '1234567890abcdef1234567890abcdef12345678' '/candidate/root'
Expand Down Expand Up @@ -120,6 +130,7 @@ if [ -f "$EV" ]; then
assert_contains 'evidence records the file count' 'file_count 2' "$EV"
assert_contains 'evidence records the protocol version' '1.0.0-rc.5' "$EV"
assert_contains 'evidence records the committed pin' "committed_released_pin $PIN" "$EV"
assert_contains 'evidence records the sole pin-promotion owner' 'pin_promotion_owner TASK-260728-d8ktna' "$EV"
else
bad 'record wrote its evidence file' "missing: $EV"
fi
Expand Down
8 changes: 8 additions & 0 deletions .github/ci/platform-cases.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
internal/buildcache TestWindowsProtectedStateMatrix windows - - Windows DACL protected-state matrix for the build cache
internal/buildcache TestWindowsProtectedStateMatrix/* - windows host-capability a subtest needing a Windows symlink may skip where the host forbids it; the matrix itself must still pass
internal/buildcache TestValidateWindowsSecurityPolicy windows - - validates the Windows security-descriptor policy on cache state
internal/buildrepo TestWindowsProtectedStoreReusesOwnerPrivateArtifact windows - - trusted owner-controlled protected external artifacts are reusable
internal/buildrepo TestWindowsProtectedStoreReusesOwnerPrivateSnapshot windows - - trusted owner-controlled protected external snapshots are reusable
internal/buildrepo TestWindowsProtectedSecurityDescriptorRejectsWrongOwnerAndDACL windows - - owner and exact protected-DACL policy rejects adversarial descriptors
internal/buildrepo TestWindowsProtectedArtifactAdversarialStateQuarantines windows - - hard-link DACL and reparse attacks force protected external artifact quarantine
internal/buildrepo TestWindowsProtectedArtifactAdversarialStateQuarantines/* - windows host-capability a reparse-point subtest may skip where the host forbids symlink creation
internal/buildrepo TestWindowsProtectedArtifactPathSwapCannotReturnBytes windows - - path replacement during retained-handle validation cannot return cache bytes
internal/buildrepo TestWindowsProtectedArtifactEntrySwapCannotReturnBytes windows - - entry-directory replacement during guarded validation cannot return cache bytes
internal/buildrepo TestWindowsProtectedSnapshotHardLinkQuarantines windows - - hard-linked protected snapshot files force quarantine

# ===========================================================================
# Windows reparse points
Expand Down
1 change: 1 addition & 0 deletions .github/ci/skip-classes.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

# --- the conformance root ---------------------------------------------------
root-unset CURATOR_CONFORMANCE_ROOT is not set deferred-only the package was deferred by suite-plan.sh
external-root-unset CURATOR_EXTERNAL_REPOSITORY_CORPUS_ROOT is not set allow the separately released rc5 external-repository corpus is exercised by rc5-external-test
root-content publishes no allow the supplied root does not publish this vector group
root-content is a pre-revision root allow the supplied root predates this artefact

Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ permissions:
env:
# The one immutable committed protocol-suite pin, referenced by every job.
# It stays on the currently qualified released revision; promoting it is
# owned by TASK-260720-38l1sy, after TASK-260720-25d05o qualifies the
# release. This pin publishes protocol 1.0.0-rc.3 and claims no later
# revision. The schema v6 candidate never appears here -- it enters through
# the non-default `candidate-conformance` job below and nowhere else.
# owned solely by TASK-260728-d8ktna after rc.5 release qualification. The
# rc.5 candidate enters only through explicit non-default candidate paths.
SPEC_PIN: 00b1688a9b2457ca397a0bb550acf47cad8ee967
# actions/setup-go only began forcing GOTOOLCHAIN=local in v6.0.0; this
# workflow pins @v5, so without these two lines a job may download a
Expand Down
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CANDIDATE := .github/ci/candidate-suite.sh
.PHONY: build test fmt lint vet check \
require-pin-root ci-test race race-full check-ci \
gate-selftest ledger-check no-broad-suppression \
candidate-verify-ref candidate-record candidate-test
candidate-verify-ref candidate-record candidate-test rc5-external-test

build:
$(GO) build -ldflags '$(LDFLAGS)' -o bin/curator ./cmd/curator
Expand Down Expand Up @@ -86,6 +86,17 @@ check-ci: require-pin-root
$(MAKE) ledger-check
$(MAKE) ci-test

# Authenticates and consumes the separately released rc.5 external-repository
# corpus. It is explicit because the protocol conformance root and the interop
# corpus are independently versioned inputs.
rc5-external-test: require-pin-root
@test -n "$(CURATOR_EXTERNAL_REPOSITORY_CORPUS_ROOT)" || { \
echo 'CURATOR_EXTERNAL_REPOSITORY_CORPUS_ROOT is required by this gate.'; \
exit 1; }
$(GO) test ./internal/conformanceconsumer ./internal/rc5interop ./cmd/curator \
-run 'TestAcceptedRC5ExternalRepositoryCorpus|TestEveryAcceptedRC5CaseHasACuratorBinding|TestNativeBlackBoxProjectGlobalLifecycle' \
-count=1

# --- Candidate protocol suite ----------------------------------------------
#
# Non-default by construction: nothing here reads or writes the committed pin.
Expand Down
122 changes: 122 additions & 0 deletions cmd/curator/native_blackbox_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)

// TestNativeBlackBoxProjectGlobalLifecycle exercises only the built command's
// public surface. It is the native project/global activation and uninstall
// spine used by rc.5 qualification; external-repository source, cache, failure,
// and rollback cases are bound separately by internal/rc5interop.
func TestNativeBlackBoxProjectGlobalLifecycle(t *testing.T) {
root := t.TempDir()
binary := filepath.Join(root, "bin", "curator")
if runtime.GOOS == "windows" {
binary += ".exe"
}
if err := os.MkdirAll(filepath.Dir(binary), 0o755); err != nil {
t.Fatal(err)
}
build := exec.Command("go", "build", "-o", binary, ".")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build black-box curator: %v\n%s", err, output)
}

home := filepath.Join(root, "manager-home")
configPath := filepath.Join(home, "config.json")
skillsRoot := filepath.Join(root, "skills")
project := filepath.Join(root, "project")
fixture := filepath.Join(skillsRoot, "native-fixture")
writeNativeFixture(t, fixture)
runNativeGit(t, project, "init", "-q", "-b", "main")

env := append(os.Environ(), "CURATOR_CONFIG="+configPath, "HOME="+root, "USERPROFILE="+root)
runNativeCurator(t, binary, env, "bootstrap", "--non-interactive", "--skills-root", skillsRoot, "--default-agents", "codex_cli")
runNativeCurator(t, binary, env, "init", project)
runNativeCurator(t, binary, env, "add", "native-fixture", "--source", "native-fixture", "--tag", "v1", "--project", project)
runNativeCurator(t, binary, env, "status", project, "--json", "--check")
runNativeCurator(t, binary, env, "shell-init", "bash", "--install", "--no-global")
if _, err := os.Stat(filepath.Join(project, ".agents", "skills", "native-fixture", "SKILL.md")); err != nil {
t.Fatalf("project activation missing: %v", err)
}

runNativeCurator(t, binary, env, "global", "init")
runNativeCurator(t, binary, env, "global", "add", "native-fixture", "--source", "native-fixture", "--tag", "v1")
runNativeCurator(t, binary, env, "global", "status", "--json", "--check")
if _, err := os.Stat(filepath.Join(home, "global", "skills", "native-fixture", "SKILL.md")); err != nil {
t.Fatalf("global activation missing: %v", err)
}

runNativeCurator(t, binary, env, "remove", "native-fixture", "--project", project)
runNativeCurator(t, binary, env, "install", project)
if _, err := os.Stat(filepath.Join(project, ".agents", "skills", "native-fixture")); !os.IsNotExist(err) {
t.Fatalf("project uninstall left installed state: %v", err)
}
runNativeCurator(t, binary, env, "global", "remove", "native-fixture")
runNativeCurator(t, binary, env, "global", "install")
if _, err := os.Stat(filepath.Join(home, "global", "skills", "native-fixture")); !os.IsNotExist(err) {
t.Fatalf("global uninstall left installed state: %v", err)
}
}

func writeNativeFixture(t *testing.T, root string) {
t.Helper()
write := func(relative, content string, mode os.FileMode) {
path := filepath.Join(root, filepath.FromSlash(relative))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), mode); err != nil {
t.Fatal(err)
}
}
write("SKILL.md", "---\nname: native-fixture\ndescription: native qualification fixture\n---\n# Native fixture\n", 0o644)
write("scripts/native-fixture", "#!/bin/sh\necho native-fixture\n", 0o755)
manifest, err := json.Marshal(map[string]any{
"schema_version": 4,
"capabilities": map[string]any{},
"runtime_roots": []string{"scripts"},
"commands": map[string]any{
"native-fixture": map[string]any{"type": "script", "unix_path": "scripts/native-fixture", "win_path": "scripts/native-fixture"},
},
})
if err != nil {
t.Fatal(err)
}
write("agent-skill.json", string(manifest), 0o644)
runNativeGit(t, root, "init", "-q", "-b", "main")
runNativeGit(t, root, "add", ".")
runNativeGit(t, root, "commit", "-qm", "native fixture")
runNativeGit(t, root, "tag", "v1")
}

func runNativeGit(t *testing.T, dir string, args ...string) {
t.Helper()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
gitArgs := append([]string{"-c", "commit.gpgsign=false", "-c", "tag.gpgSign=false"}, args...)
command := exec.Command("git", gitArgs...)
command.Dir = dir
command.Env = append(os.Environ(),
"GIT_AUTHOR_NAME=Curator Test", "GIT_AUTHOR_EMAIL=curator@example.test",
"GIT_COMMITTER_NAME=Curator Test", "GIT_COMMITTER_EMAIL=curator@example.test",
)
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v\n%s", args, err, output)
}
}

func runNativeCurator(t *testing.T, binary string, env []string, args ...string) {
t.Helper()
command := exec.Command(binary, args...)
command.Env = env
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("curator %v: %v\n%s", args, err, output)
}
}
Loading
Loading