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
1 change: 1 addition & 0 deletions .echo/sdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"echo.certforge.governance.get",
"echo.certforge.governance.update",
"echo.certforge.health",
"echo.certforge.identities",
"echo.certforge.list_runs",
"echo.certforge.members.activate",
"echo.certforge.members.deactivate",
Expand Down
1 change: 1 addition & 0 deletions contracts/certforge-capability-surface.v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"GET /v1/certifications": "echo.certforge.list_runs",
"POST /v1/certifications": "echo.certforge.submit",
"GET /v1/certifications/{run_id}": "echo.certforge.status",
"GET /v1/certifications/{run_id}/identities": "echo.certforge.identities",
"POST /v1/certifications/{run_id}/cancel": "echo.certforge.cancel",
"GET /v1/certifications/{run_id}/findings": "echo.certforge.findings",
"GET /v1/certifications/{run_id}/evidence": "echo.certforge.evidence",
Expand Down
205 changes: 204 additions & 1 deletion contracts/certforge-sdk-capabilities.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,209 @@
"type": "object"
}
},
"echo.certforge.identities": {
"input_schema": {
"additionalProperties": false,
"properties": {
"command": {
"maxLength": 64,
"minLength": 1,
"type": "string"
},
"run_id": {
"type": "string"
}
},
"required": [
"command",
"run_id"
],
"type": "object"
},
"operation": "GET /v1/certifications/{run_id}/identities",
"output_schema": {
"additionalProperties": false,
"properties": {
"environment_identity_digest": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"export_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"identities": {
"additionalProperties": false,
"properties": {
"environment": {
"additionalProperties": false,
"properties": {
"adapter_set_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"egress_policy_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"harness_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"model_route_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"os_runtime_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"policy_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"prompt_set_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"runner_image_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"test_plan_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
}
},
"required": [
"runner_image_sha256",
"adapter_set_sha256",
"test_plan_sha256",
"policy_sha256",
"harness_sha256",
"prompt_set_sha256",
"model_route_sha256",
"os_runtime_sha256",
"egress_policy_sha256"
],
"type": "object"
},
"target": {
"additionalProperties": false,
"properties": {
"artifact_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"canonical_ref": {
"maxLength": 2048,
"minLength": 1,
"type": "string"
},
"configuration_sha256": {
"anyOf": [
{
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
{
"type": "null"
}
]
},
"dependency_sha256": {
"anyOf": [
{
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
{
"type": "null"
}
]
},
"source_commit": {
"anyOf": [
{
"pattern": "^[0-9a-fA-F]{40}(?:[0-9a-fA-F]{24})?$",
"type": "string"
},
{
"type": "null"
}
]
},
"target_type": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
"type": "string"
},
"tenant_id": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
"type": "string"
}
},
"required": [
"tenant_id",
"target_type",
"canonical_ref",
"artifact_sha256",
"source_commit",
"dependency_sha256",
"configuration_sha256"
],
"type": "object"
}
},
"required": [
"target",
"environment"
],
"type": "object"
},
"identities_sha256": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"rule_manifest_digest": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"rule_manifest_id": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
"type": "string"
},
"run_id": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
"type": "string"
},
"schema_version": {
"const": "certforge.canonical-identities.v1",
"type": "string"
},
"target_identity_digest": {
"pattern": "^[0-9a-f]{64}$",
"type": "string"
},
"tenant_id": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
"type": "string"
}
},
"required": [
"schema_version",
"run_id",
"tenant_id",
"target_identity_digest",
"environment_identity_digest",
"rule_manifest_id",
"rule_manifest_digest",
"identities",
"identities_sha256",
"export_sha256"
],
"type": "object"
}
},
"echo.certforge.list_runs": {
"input_schema": {
"additionalProperties": false,
Expand Down Expand Up @@ -2295,7 +2498,7 @@
}
}
},
"capability_count": 60,
"capability_count": 61,
"policy": "Every Certification Forge SDK action has a command-bearing strict input schema and an output schema derived from its production route contract.",
"schema_version": "1.0.0"
}
75 changes: 75 additions & 0 deletions docs/CANONICAL_IDENTITY_EXPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Canonical identity export

`GET /v1/certifications/{run_id}/identities` exports the two canonical identity
objects already stored for one certification run. This is a metadata projection;
it does not reconstruct a target, read evidence artifacts, issue an attestation,
change a verdict, or admit a deployment.

## Authorization and integrity

The route requires configured subscriber governance and an authenticated Bearer
principal with `runs:read`. The existing tenant consistency check and tenant-scoped
store lookup apply. A tenant header by itself is insufficient, including when
subscriber governance is disabled. Unknown and foreign runs both return 404.

The exporter requires every field of the stored `TargetIdentity` and
`EnvironmentIdentity`, validates their models, and recomputes both identity
digests. Declared identities and records whose objects do not match their stored
digests return 409. Duplicate fields, oversized records, unknown fields, invalid
source revisions, and references containing credentials, query material,
fragments, or control characters are refused. Encoded reference material is
checked conservatively. The exporter never silently redacts an identity and
substitutes a different digest.

Before returning data, the route rechecks the live principal through the existing
resource audit operation. Revoked keys, changed permissions, and audit failures
prevent the export. The audit records only the run identifier and four digests;
canonical references and object contents are not audit details. Responses use
`Cache-Control: no-store`. Run rows, evidence, signatures, and policy stay unchanged.

## Response and collector handoff

The strict response schema is `certforge.canonical-identities.v1`. It contains:

- `run_id`, `tenant_id`, `rule_manifest_id`, and `rule_manifest_digest`;
- the stored `target_identity_digest` and `environment_identity_digest`;
- `identities`, with exactly `target` and `environment`;
- `identities_sha256`, the SHA256 of canonical JSON for `identities`;
- `export_sha256`, the SHA256 of canonical JSON for the response excluding
`export_sha256` itself.

Canonical JSON uses sorted keys, separators `(',', ':')`, UTF-8,
`ensure_ascii=False`, and no trailing newline. An admitted consumer verifies the
run/tenant and both digests against its independently selected certification run,
verifies `export_sha256`, and writes exactly the canonical `identities` bytes.
`identities_sha256` is then the exact file hash for the separately reviewed
production-E2E intake's `--identities-sha256` argument. The identity document has
only `target` and `environment`, as required by that intake.

These hashes establish content consistency. They are not collector signatures,
release authority, or evidence that production journeys occurred. The existing
collector trust, freshness, real journey, external acceptance, and verdict checks
remain mandatory. No identity values are supplied as hand-written examples.

## SDK contract and admission

The proposed capability is `echo.certforge.identities`, a GET path operation with
`command` and `run_id` as required inputs, `certforge.read` as its gateway scope,
and danger tier 0. Its complete output schema is generated from the FastAPI route.
The registration proposal retains the same server-held tenant header and vaulted
subscriber credential reference as `echo.certforge.status`. No credential value
is placed in the request, response, source change, or documentation.

Source declarations do not activate this capability. Native admission must verify
the exact issuer revision, trusted runtime, deployed route, registered descriptor,
and current HTTP handler's forwarding of the server-held Bearer/tenant binding.
Require an authenticated positive read of the intended run, wrong-tenant and
revoked-key negatives, matching digests, and a committed audit event. Do not infer
wrapper readiness solely from a registration row or schema. Existing evidence
artifact scopes, collector keys, service protections, and deployment policy must
remain unchanged.

The service and generated contract repairs are isolated source work until that
admitted rollout and readback succeed. The separate production-E2E intake must
also be independently reviewed and admitted; this change neither installs it nor
selects or provisions a collector signer.
4 changes: 2 additions & 2 deletions scripts/certification_journey.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def main() -> int:

sdk = json.loads((ROOT / ".echo" / "sdk.json").read_text(encoding="utf-8"))
capabilities = sdk.get("capabilities", [])
require(len(capabilities) == 60, "SDK capability declaration must contain exactly 60 entries")
require(len(set(capabilities)) == 60, "SDK capability declaration contains duplicates")
require(len(capabilities) == 61, "SDK capability declaration must contain exactly 61 entries")
require(len(set(capabilities)) == 61, "SDK capability declaration contains duplicates")
require(all(isinstance(item, str) and item.startswith("echo.") for item in capabilities),
"SDK capability declaration contains an invalid identifier")

Expand Down
6 changes: 3 additions & 3 deletions scripts/master_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ def _p7(value: dict[str, Any]) -> None:

def _sdk(value: dict[str, Any]) -> None:
capabilities = value.get("capabilities")
_require(value.get("capability_count") == 60, "SDK contract must contain exactly 60 capabilities")
_require(isinstance(capabilities, dict) and len(capabilities) == 60, "SDK capability rows are incomplete")
_require(value.get("capability_count") == 61, "SDK contract must contain exactly 61 capabilities")
_require(isinstance(capabilities, dict) and len(capabilities) == 61, "SDK capability rows are incomplete")
names = list(capabilities)
_require(len(set(names)) == 60, "SDK capability IDs are not unique")
_require(len(set(names)) == 61, "SDK capability IDs are not unique")
_require(all(isinstance(name, str) and name.startswith("echo.cert") for name in names), "SDK capability namespace drift")
_require(
all(
Expand Down
7 changes: 7 additions & 0 deletions scripts/register_certforge_caps.sql
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ VALUES
'{"type":"object","required":["run_id"],"properties":{"run_id":{"type":"string"}}}'::jsonb,
'certforge.read', 0, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'),

-- New identity metadata route: activate only after admitted service rollout and authenticated smoke.
('echo.certforge.identities',
'Certification Forge: export reconciled canonical run identities after tenant authorization, digest verification, and metadata-only audit. Never reads evidence bytes.',
'http', 'http://127.0.0.1:8309/v1/certifications/{run_id}/identities', 'GET', 'path', 'forge',
'{"type":"object","required":["command","run_id"],"properties":{"command":{"type":"string","minLength":1,"maxLength":64},"run_id":{"type":"string"}},"additionalProperties":false}'::jsonb,
'certforge.read', 0, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'),

('echo.certforge.cancel',
'Certification Forge: cancel a certification run. Fail-closed — 409 once past the cancellable window; never re-opens a terminal run.',
'http', 'http://127.0.0.1:8309/v1/certifications/{run_id}/cancel', 'POST', 'path', 'forge',
Expand Down
Loading
Loading