diff --git a/CHANGELOG.md b/CHANGELOG.md index ba82bc8..c9b47b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Versioning. ## [Unreleased] +- Observe installed Devin CLI as the eighth catalogue identity while retaining + seven verified execution adapters. Bind per-mapping runtime evidence policy + and refuse unproven Devin configuration projections or runtime-proof claims. + - Converge the ci-workflows reusable callers on the signed 0.1.26 tag (`a7b90bd`); the `gds-ci` projection pin moves through its canonical `ci.workflow_ref` input. diff --git a/README.md b/README.md index 41b56bf..11484e7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ also runs the source-bound 2000-repository assurance scenario and requires current source evidence before any artifact can be attested. It does **not** prove harness runtime behaviour, and no longer claims to. The -seven harnesses are registered here with delegated runtime evidence; their +seven execution adapters are registered here with delegated runtime evidence; their runtime suites live in the private `NDDev-it-com/setup-systems` repository, which `harnesses/module-bridge.yaml` names as the evidence owner. Each profile records `runtime_tests.last_result: delegated`, and @@ -102,4 +102,8 @@ claiming `delegated` that the bridge does not map is rejected evidence here nor delegates it (`GDS_HARNESS_RUNTIME_UNOWNED`). Promoting a harness to `supported` still requires a local `pass`. +The eighth catalogue identity, Devin, is installed-only and provisional. GDS +observes its version but neither configures it nor claims model/runtime proof. +Its bootstrap mapping explicitly declares unproven evidence (ADR 0041). + Architecture and contracts are in `docs/architecture/` and `docs/contracts/`. diff --git a/core/harness/devin_test.go b/core/harness/devin_test.go new file mode 100644 index 0000000..7d699c8 --- /dev/null +++ b/core/harness/devin_test.go @@ -0,0 +1,70 @@ +package harness + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/NDDev-OpenNetwork/github-device-sync/core/validation" +) + +func TestDevinVersionDetectionDoesNotConfigureTheHarness(t *testing.T) { + root := repoRootForTest(t) + schemas, err := validation.NewSchemaSet() + if err != nil { + t.Fatal(err) + } + bin := t.TempDir() + program := filepath.Join(bin, "devin") + if err := os.WriteFile(program, []byte("#!/bin/sh\n[ \"$1\" = --version ] || exit 42\nprintf 'devin 1.2.3\\n'\n"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", bin) + home := t.TempDir() + t.Setenv("HOME", home) + observation, findings := Detect(context.Background(), root, "devin", schemas) + if len(findings) != 0 || observation.Result != "observed" || observation.Version != "devin 1.2.3" { + t.Fatalf("Devin observation=%+v findings=%+v", observation, findings) + } + entries, err := os.ReadDir(home) + if err != nil || len(entries) != 0 { + t.Fatalf("version observation changed the home directory: %v %v", entries, err) + } +} + +func TestDevinRegistryDoesNotClaimDelegatedProofOrInstallConfiguration(t *testing.T) { + root := repoRootForTest(t) + schemas, err := validation.NewSchemaSet() + if err != nil { + t.Fatal(err) + } + report, findings := ValidateStatic(root, "devin", schemas) + if len(findings) != 0 || report.CapabilityStatus != "provisional" || report.RuntimeEvidence != "not-proven" || report.RuntimeEvidenceOwner != "" { + t.Fatalf("unproven Devin capabilities were promoted: %+v %+v", report, findings) + } + adapter, findings := NewAdapter(root, "devin", schemas) + if len(findings) != 0 { + t.Fatal(findings) + } + target := t.TempDir() + _, findings = adapter.PlanInstall(target, RenderRequest{SkillProfile: "core", Scope: "project"}) + if !containsHarnessFinding(findings, "GDS_HARNESS_PROJECT_SKILLS_NOT_PROVEN") { + t.Fatalf("observation-only Devin produced a configuration plan: %+v", findings) + } + entries, err := os.ReadDir(target) + if err != nil || len(entries) != 0 { + t.Fatalf("refused adapter plan changed the target: %v %v", entries, err) + } +} + +func TestExplicitDevinRuntimeProofRemainsUnproven(t *testing.T) { + schemas, err := validation.NewSchemaSet() + if err != nil { + t.Fatal(err) + } + _, findings := Validate(repoRootForTest(t), "devin", schemas) + if !containsHarnessFinding(findings, "GDS_HARNESS_RUNTIME_UNOWNED") { + t.Fatalf("an explicit runtime request accepted an installed-only CLI: %+v", findings) + } +} diff --git a/core/harness/module_bridge.go b/core/harness/module_bridge.go index 59439ca..1fbc158 100644 --- a/core/harness/module_bridge.go +++ b/core/harness/module_bridge.go @@ -26,10 +26,11 @@ type ModuleBridgeDocument struct { } type ModuleHarnessMapping struct { - ModuleID string `json:"module_id"` - HarnessID string `json:"harness_id"` - Lifecycle string `json:"lifecycle"` - ModuleAliases []string `json:"module_aliases,omitempty"` + ModuleID string `json:"module_id"` + HarnessID string `json:"harness_id"` + Lifecycle string `json:"lifecycle"` + ModuleAliases []string `json:"module_aliases,omitempty"` + RuntimeEvidence string `json:"runtime_evidence,omitempty"` } type BridgeConsumer struct { @@ -123,6 +124,12 @@ func validateModuleBridgeSemantics( } } for _, mapping := range document.Mappings { + if mapping.RuntimeEvidence != "" && mapping.RuntimeEvidence != "delegated" && mapping.RuntimeEvidence != "not-proven" { + findings = append(findings, bridgeFinding( + "GDS_MODULE_BRIDGE_EVIDENCE_POLICY_INVALID", "Module mapping has an unknown runtime evidence policy.", + map[string]any{"harness_id": mapping.HarnessID, "runtime_evidence": mapping.RuntimeEvidence}, + )) + } findings = append(findings, claimBridgeIdentity(seenModule, mapping.ModuleID, mapping.ModuleID, "MODULE_ID")..., ) diff --git a/core/harness/module_bridge_test.go b/core/harness/module_bridge_test.go index a3a504a..f565b92 100644 --- a/core/harness/module_bridge_test.go +++ b/core/harness/module_bridge_test.go @@ -39,6 +39,11 @@ func TestCanonicalModuleBridgeCoversEveryHarnessAndDigestsDeterministically(t *t if changed == first.IdentityDigest { t.Fatal("identity digest did not bind lifecycle identity") } + mutated.Mappings[0].RuntimeEvidence = "not-proven" + changedEvidence, err := moduleBridgeIdentityDigest(mutated) + if err != nil || changedEvidence == changed { + t.Fatalf("identity digest did not bind runtime evidence policy: %s %v", changedEvidence, err) + } } func TestModuleBridgeIdentityDigestIgnoresOrderingButBindsIdentityDrift(t *testing.T) { @@ -262,7 +267,7 @@ func TestSelectedRuntimeValidationReportsDelegationRatherThanSilentSuccess(t *te if err != nil { t.Fatal(err) } - report, findings := ValidateSelected(root, CanonicalIDs, schemas) + report, findings := ValidateSelected(root, WorkPolicyActiveIDs, schemas) if len(findings) != 0 { t.Fatalf("findings = %#v", findings) } @@ -270,6 +275,12 @@ func TestSelectedRuntimeValidationReportsDelegationRatherThanSilentSuccess(t *te t.Fatalf("harness count = %d, want %d", len(report.Harnesses), len(CanonicalIDs)) } for _, item := range report.Harnesses { + if item.Harness == "devin" { + if item.RuntimeEvidence != "not-proven" || item.RuntimeEvidenceOwner != "" || item.CapabilityStatus != "provisional" { + t.Fatalf("installed-only Devin inherited execution proof: %+v", item) + } + continue + } if item.RuntimeEvidence != "delegated" { t.Fatalf("%s runtime evidence = %q, want delegated", item.Harness, item.RuntimeEvidence) } diff --git a/core/harness/registry.go b/core/harness/registry.go index 9d2a5f3..9d9bd3e 100644 --- a/core/harness/registry.go +++ b/core/harness/registry.go @@ -17,14 +17,14 @@ var CanonicalIDs = []string{ "claude-code", "codex", "cursor", + "devin", "grok-build", "opencode", "pi", } -// WorkPolicyActiveIDs is the global execution/release allowlist. It equals -// CanonicalIDs: every catalogued harness is delivered by a setup system, so -// there is no longer an on-pause remainder to separate. +// WorkPolicyActiveIDs is the execution/configuration allowlist. The catalogue +// also includes installed-only harnesses whose adapter behavior is not proven. var WorkPolicyActiveIDs = []string{ "antigravity", "claude-code", @@ -209,7 +209,11 @@ func validateAll( for _, alias := range entry.LegacyAliases { report.Aliases[alias] = entry.ID } - entryRuntime := includeRuntime && (selected == nil || selected[entry.ID]) + // A mapping declaring installation-only coverage is observed statically + // in all-mode. An explicit runtime request for that identity remains + // unproven, rather than inheriting another harness's delegated proof. + entryRuntime := includeRuntime && (selected == nil || selected[entry.ID]) && + (entry.RuntimeEvidence != "not-proven" || selected != nil && selected[entry.ID]) var item Report var profile CapabilityProfile var itemFindings []domain.Finding @@ -324,6 +328,9 @@ func delegationIndex(bridge ModuleBridgeDocument) map[string]string { } index := make(map[string]string, len(bridge.Mappings)) for _, mapping := range bridge.Mappings { + if mapping.RuntimeEvidence == "not-proven" { + continue + } index[mapping.HarnessID] = bridge.EvidenceOwner.Repository } return index diff --git a/core/harness/work_policy_test.go b/core/harness/work_policy_test.go index 6e9724c..5edfae7 100644 --- a/core/harness/work_policy_test.go +++ b/core/harness/work_policy_test.go @@ -1,21 +1,26 @@ package harness -import "testing" +import ( + "slices" + "testing" +) // The catalogue and the work-policy allowlist were once different sizes: ten // catalogued harnesses had no setup system and were carried on-pause. Every -// catalogued harness is now delivered by one of the seven setup systems, so the -// two sets are identical and nothing can be catalogued-but-paused. A harness -// that is not in the catalogue is rejected as unknown, not as paused. -func TestWorkPolicyCatalogueEqualsActiveSeven(t *testing.T) { - if len(CanonicalIDs) != 7 || len(WorkPolicyActiveIDs) != 7 { - t.Fatalf("catalogue=%d active=%d, want 7 and 7", len(CanonicalIDs), len(WorkPolicyActiveIDs)) +// seven execution adapters retain their proof requirements. An installed-only +// CLI may be observed without being selected for configuration or execution. +func TestWorkPolicyCatalogueIncludesObservedDevinAndActiveSeven(t *testing.T) { + if len(CanonicalIDs) != 8 || len(WorkPolicyActiveIDs) != 7 { + t.Fatalf("catalogue=%d active=%d, want 8 and 7", len(CanonicalIDs), len(WorkPolicyActiveIDs)) } - for index, id := range CanonicalIDs { - if WorkPolicyActiveIDs[index] != id { - t.Fatalf("catalogue and active set diverge at %d: %q vs %q", index, id, WorkPolicyActiveIDs[index]) + for _, id := range WorkPolicyActiveIDs { + if !slices.Contains(CanonicalIDs, id) { + t.Fatalf("active identity %q is not catalogued", id) } } + if findings := ValidateDeviceSelection([]string{"devin"}); len(findings) != 1 || findings[0].Code != "GDS_DEVICE_HARNESS_PAUSED" { + t.Fatalf("observation-only Devin must not be selected as a configured adapter: %+v", findings) + } findings := ValidateDeviceSelection([]string{"codex", "zcode"}) if len(findings) != 1 || findings[0].Code != "GDS_HARNESS_SELECTED_UNKNOWN" || findings[0].Evidence["harness"] != "zcode" { diff --git a/docs/adr/0041-installed-harnesses-and-execution-adapters.md b/docs/adr/0041-installed-harnesses-and-execution-adapters.md new file mode 100644 index 0000000..7f816d6 --- /dev/null +++ b/docs/adr/0041-installed-harnesses-and-execution-adapters.md @@ -0,0 +1,33 @@ +# ADR 0041: Installed harnesses and execution adapters + +Status: Accepted + +Date: 2026-10-08 + +Supersedes: ADR 0037 (the exact catalogue membership only) + +## Context + +Devin CLI is distributed by the public bootstrap, but GDS rejected its identity +as unknown. Registering an installed program must not imply that its skill, +instruction, hook, model or configuration behavior has passed adapter tests. + +## Decision + +- The canonical catalogue includes eight identities. Devin is provisional and + supports bounded version observation; its runtime evidence is `not-proven`. +- The seven verified execution adapters remain in `WorkPolicyActiveIDs`. + Selecting Devin for adapter synchronization reports `installed-paused`. +- The Devin module bridge maps to `macos-ubuntu-bootstrap` and explicitly + declares `runtime_evidence: not-proven`. This mapping does not inherit the + setup-systems runtime evidence owner. +- Default registry-wide validation retains the observation-only entry without + promoting it. An explicit request for Devin runtime proof remains unproven. +- Adapter installation refuses unproven project skill paths. Version detection + performs no model call and creates no vendor configuration. + +## Verification + +Synthetic version observation, empty-target adapter refusal, explicit runtime +proof refusal and registry/work-policy separation are exercised in Go tests. +The module bridge digest binds the per-mapping evidence policy. diff --git a/docs/adr/README.md b/docs/adr/README.md index ec8134d..00c4fbe 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,6 +8,7 @@ clause stops being normative. | ADR | Title | Status | Supersedes | Superseded by | |---|---|---|---|---| +| [0041](0041-installed-harnesses-and-execution-adapters.md) | Installed harnesses and execution adapters | Accepted | ADR 0037 | — | | [0040](0040-tenancy-is-the-provider-account.md) | Tenancy is the provider account | Accepted | — | — | | [0039](0039-repo-scoped-session-evidence.md) | Repo-scoped session evidence | Accepted | — | — | | [0038](0038-release-identity-carries-no-channel.md) | Release identity carries no channel | Accepted | ADR 0016 | — | diff --git a/docs/contracts/harness-adapters-v1.md b/docs/contracts/harness-adapters-v1.md index 60e9a30..3f0121b 100644 --- a/docs/contracts/harness-adapters-v1.md +++ b/docs/contracts/harness-adapters-v1.md @@ -18,6 +18,7 @@ antigravity claude-code codex cursor +devin grok-build opencode pi diff --git a/harnesses/README.md b/harnesses/README.md index 132320f..a3192b0 100644 --- a/harnesses/README.md +++ b/harnesses/README.md @@ -1,11 +1,12 @@ # Harness capability registry -`capability-registry.yaml` is the canonical index for the seven agent harnesses -this estate installs, one per setup system in `NDDev-OpenNetwork`. Each profile -records only current official documentation facts. +`capability-registry.yaml` is the canonical index for eight harness identities. +Seven have execution adapters delivered by setup systems. Devin is registered +for bounded installed-version observation through the public bootstrap. -The catalogue and the work-policy allowlist are the same set, so nothing can be -catalogued but paused. ADR 0037 records why the catalogue is seven. +The execution/configuration allowlist remains the seven proven adapters. +Devin is provisional and cannot install a GDS configuration projection. +ADR 0041 records this separation; ADR 0037 retains the historical decision. Canonical harness identities are: @@ -14,6 +15,7 @@ Canonical harness identities are: - `claude-code`; - `codex`; - `cursor`; +- `devin`; - `grok-build`; - `opencode`; - `pi`. diff --git a/harnesses/capability-registry.yaml b/harnesses/capability-registry.yaml index 97df8e8..f5d1a29 100644 --- a/harnesses/capability-registry.yaml +++ b/harnesses/capability-registry.yaml @@ -33,6 +33,13 @@ harnesses: + - id: "devin" + status: "provisional" + profile: "harnesses/devin/profile.yaml" + verified_at: "2026-10-08" + runtime_evidence: "not-proven" + legacy_aliases: [] + - id: "grok-build" status: "supported" profile: "harnesses/grok-build/profile.yaml" @@ -59,5 +66,3 @@ harnesses: runtime_evidence: "delegated" legacy_aliases: [] - - diff --git a/harnesses/devin/profile.yaml b/harnesses/devin/profile.yaml new file mode 100644 index 0000000..6876560 --- /dev/null +++ b/harnesses/devin/profile.yaml @@ -0,0 +1,34 @@ +schema_version: 1 + +harness_profile: + id: "devin" + product: "devin" + status: "provisional" + aliases: [] + capability_version: "2026-10-08" + verified_at: "2026-10-08" + official_sources: + - "https://docs.devin.ai/cli" + - "https://docs.devin.ai/cli/extensibility" + detection: + command_candidates: ["devin"] + version_arguments: ["--version"] + instructions: + native_agents: true + nested_chain: "custom" + imports: true + skills: + standard: "none" + native_paths: [] + symlinks: false + explicit_only: + mechanism: "not-proven" + projection: + instruction_strategy: "not-proven" + skill_strategy: "not-proven" + hooks: + supported: false + lifecycle: [] + runtime_tests: + required: false + last_result: "not-proven" diff --git a/harnesses/module-bridge.yaml b/harnesses/module-bridge.yaml index b5bf387..5df94e8 100644 --- a/harnesses/module-bridge.yaml +++ b/harnesses/module-bridge.yaml @@ -9,7 +9,7 @@ evidence_owner: runtime_tests: required: false -# Seven harnesses, seven setup systems, one mapping each. The retired harness +# Seven execution adapters and one installed-only CLI, one mapping each. The retired harness # application line is archived and is deliberately not named in this file -- # not as a module, not as an alias. A bridge is a statement about what exists; # ADR 0037 holds the history. @@ -30,6 +30,10 @@ mappings: - module_id: "cursor-setup-system" harness_id: "cursor" lifecycle: "active" + - module_id: "macos-ubuntu-bootstrap" + harness_id: "devin" + lifecycle: "active" + runtime_evidence: "not-proven" - module_id: "grok-setup-system" harness_id: "grok-build" lifecycle: "active" diff --git a/schemas/v1/module-harness-bridge.schema.json b/schemas/v1/module-harness-bridge.schema.json index d59ac77..ea43552 100644 --- a/schemas/v1/module-harness-bridge.schema.json +++ b/schemas/v1/module-harness-bridge.schema.json @@ -76,6 +76,10 @@ "lifecycle": { "enum": ["active", "renamed", "retired"] }, + "runtime_evidence": { + "description": "Optional per-mapping runtime proof policy. Missing retains delegated behavior; not-proven declares installation-only coverage.", + "enum": ["delegated", "not-proven"] + }, "module_aliases": { "type": "array", "uniqueItems": true, diff --git a/tests/harness/runtime-contract.yaml b/tests/harness/runtime-contract.yaml index a5c23e8..2ad2a38 100644 --- a/tests/harness/runtime-contract.yaml +++ b/tests/harness/runtime-contract.yaml @@ -6,6 +6,7 @@ harnesses: - "claude-code" - "codex" - "cursor" + - "devin" - "grok-build" - "opencode" - "pi"