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 @@ -29,6 +29,7 @@
"echo.certforge.evidence",
"echo.certforge.findings",
"echo.certforge.governance.get",
"echo.certforge.governance.reconcile_reservation",
"echo.certforge.governance.update",
"echo.certforge.health",
"echo.certforge.list_runs",
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 @@ -34,6 +34,7 @@
"POST /v1/subscriber/subscription/cancel": "echo.certforge.subscription.cancel",
"GET /v1/subscriber/governance": "echo.certforge.governance.get",
"PUT /v1/subscriber/governance": "echo.certforge.governance.update",
"POST /v1/subscriber/certifications/{run_id}/reconcile-reservation": "echo.certforge.governance.reconcile_reservation",
"POST /v1/subscriber/policy-packs": "echo.certforge.policy_packs.create",
"GET /v1/subscriber/policy-packs": "echo.certforge.policy_packs.list",
"POST /v1/subscriber/private-workers": "echo.certforge.private_workers.create",
Expand Down
32 changes: 31 additions & 1 deletion contracts/certforge-sdk-capabilities.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,36 @@
"type": "object"
}
},
"echo.certforge.governance.reconcile_reservation": {
"input_schema": {
"additionalProperties": false,
"properties": {
"command": {
"maxLength": 64,
"minLength": 1,
"type": "string"
},
"reason": {
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
"type": "string"
},
"run_id": {
"type": "string"
}
},
"required": [
"command",
"reason",
"run_id"
],
"type": "object"
},
"operation": "POST /v1/subscriber/certifications/{run_id}/reconcile-reservation",
"output_schema": {
"additionalProperties": true,
"type": "object"
}
},
"echo.certforge.governance.update": {
"input_schema": {
"additionalProperties": false,
Expand Down Expand Up @@ -2322,7 +2352,7 @@
}
}
},
"capability_count": 61,
"capability_count": 62,
"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"
}
95 changes: 95 additions & 0 deletions docs/RELEASED_RESERVATION_RECONCILIATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Reconcile expired ownership on a released terminal reservation

Historical terminal reconciliation could set a reservation to `RELEASED` while
retaining its claim and lease. Those fields do not authorize a released worker;
their retention is a metadata-cleanup defect. The initial maintenance guard used
their presence as a conservative proxy for possible orphan work. A separately
reviewed activity classifier can establish quiescence from canonical terminal,
lease, dispatch and process-ownership evidence while retaining the anomaly and
cleanup obligation. This change does not alter that guard or waive cleanup.
Normal expired-claim recovery selects only `EXECUTING`/`STARTED`, and an ordinary
release is already a no-op for `RELEASED`, so neither repairs this residue.

Normal terminal reconciliation now clears claim and lease fields in the same
existing transaction that releases the reservation. No run state, dispatch,
usage charge, compensation, historical heartbeat or signing field is changed by
that additional cleanup.

For existing residue, the supported owner API is:

```http
POST /v1/subscriber/certifications/{run_id}/reconcile-reservation
X-Tenant-ID: <existing authorized organization>
Authorization: Bearer <existing owner/admin key with governance:manage>
Content-Type: application/json

{"reason":"expired_terminal_claim_owner_repair"}
```

This uses the existing `governance:manage` permission and its existing role and
key-scope policy. It does not create a permission, credential or scope grant.
The source declares `echo.certforge.governance.reconcile_reservation` with the
existing `certforge.governance.manage` SDK scope, tier2 and server-side Vault
credential reference. Its strict command-bearing schema is generated through
the existing contract generator. This declaration is not live registration.
The exact source-declared SDK set increases from 61 to 62 capabilities; manifest,
generated schemas and exact-count validators stay synchronized and strict.
The library entry point is
`SubscriberGovernance.reconcile_released_reservation(principal, run_id=..., reason=...)`.
It repeats live key, membership, role, organization, subscription and scope
authorization under the same immediate write transaction as the repair.

The method accepts exactly one reservation joined to a run in the principal's
tenant. It requires reservation `RELEASED` and run `COMPLETED`, `CANCELLED` or
`INFRASTRUCTURE_FAILURE`. An existing dispatch must be terminal and unclaimed.
For residual ownership, the stored lease must be parseable, timezone-aware and
expired under the canonical clock. A claim without proof of lease expiry is
refused. Active, unexpired, nonterminal, ambiguous or cross-tenant rows cannot
be repaired through this operation.

The transaction clears only `claim_token`, `lease_expires_at` and the reservation
update time, then reads back the cleared fields. SQL selects only the boolean
presence of a claim; neither API output nor audit contains a claim token or
idempotency key. It preserves run/outcome, dispatch, usage/compensation and all
other reservations. The existing audit chain records one
`certification.released_claim_reconcile` event with reason, terminal state,
prior ownership-presence flags and `meter_compensated:false`. Audit failure rolls
back the cleanup. A clean repeat returns `reconciled:false` with unchanged
reservation metadata and creates no second domain reconciliation event. Normal
API request-outcome auditing still applies to each request.

The response schema is
`echo.certforge.released-reservation-reconciliation.v1`. It reports tenant/run,
terminal run state, released reservation state, whether cleanup occurred,
claim/lease absence, durable reservation update time and uncompensated metering.
Unknown or other-tenant run IDs return the same `run_reservation_not_found`.

## Admission and operation boundaries

This source candidate is based on installed commit
`d8ad9fd881365801354b78a758632d2a31ab6828`; it is not an installed repair. The
existing d8ad API does not acquire this endpoint merely because source tests
pass. No SDK registration SQL is applied by this source change.

Normal delivery requires independent source acceptance, the required local and
hosted checks, exact-candidate CertForge and Certification App evidence, and
the normal owner release/readiness and deployment/rollback admission applicable
to the changed service. Any installed source change invalidates old exact-source
commitments: refresh the installed source closure, readiness and owner guard
bindings rather than relabeling d8ad evidence. Until then, do not import this
candidate into the live service, autoload it, mutate the live database directly,
or weaken the quiet guard.

Once the exact implementation and owner route are admitted, the owner must first
read the exact tenant/run/reservation/dispatch, prove no active work, preserve the
pending-work projection and metering/history fingerprints, then use the ordinary
authenticated API. Read back the exact response, canonical row/audit state and
all protected projections; repeat once to prove idempotence. Existing grants
must cover `governance:manage`; absent permission is an admission dependency,
never a reason to mint or broaden a key implicitly.

Rollback of this source candidate is an ordinary scoped revert or retention of
the previously admitted service revision. A later successful terminal-ownership
cleanup must not be undone by restoring an old claim token or rewriting history.
Live service rollback and any database recovery require their separately tested
owner procedures; this source change performs neither.
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) == 61, "SDK capability declaration must contain exactly 61 entries")
require(len(set(capabilities)) == 61, "SDK capability declaration contains duplicates")
require(len(capabilities) == 62, "SDK capability declaration must contain exactly 62 entries")
require(len(set(capabilities)) == 62, "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
11 changes: 8 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") == 61, "SDK contract must contain exactly 61 capabilities")
_require(isinstance(capabilities, dict) and len(capabilities) == 61, "SDK capability rows are incomplete")
_require(value.get("capability_count") == 62, "SDK contract must contain exactly 62 capabilities")
_require(isinstance(capabilities, dict) and len(capabilities) == 62, "SDK capability rows are incomplete")
names = list(capabilities)
_require(len(set(names)) == 61, "SDK capability IDs are not unique")
_require(len(set(names)) == 62, "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 All @@ -87,6 +87,11 @@ def _sdk(value: dict[str, Any]) -> None:
),
"SDK capability contract rows are malformed",
)
_require(
capabilities.get("echo.certforge.governance.reconcile_reservation", {}).get("operation")
== "POST /v1/subscriber/certifications/{run_id}/reconcile-reservation",
"SDK released-reservation owner route is missing or mismatched",
)


def _ci(value: dict[str, Any], source_commit: str) -> None:
Expand Down
2 changes: 2 additions & 0 deletions scripts/register_certforge_caps.sql
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ VALUES
'{"type":"object","additionalProperties":false}'::jsonb, 'certforge.governance.read', 0, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'),
('echo.certforge.governance.update', 'Update tenant governance with optimistic version control.', 'http', 'http://127.0.0.1:8309/v1/subscriber/governance', 'PUT', 'json_body', 'forge',
'{"type":"object","required":["expected_version","config"],"properties":{"expected_version":{"type":"integer"},"config":{"type":"object"}},"additionalProperties":false}'::jsonb, 'certforge.governance.manage', 2, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 20, 'active', 'unknown'),
('echo.certforge.governance.reconcile_reservation', 'Reconcile only expired residual claim and lease metadata on one tenant-owned released terminal reservation. Existing governance authority is rechecked transactionally; no execution, state transition or quota compensation.', 'http', 'http://127.0.0.1:8309/v1/subscriber/certifications/{run_id}/reconcile-reservation', 'POST', 'path', 'forge',
'{"type":"object","required":["run_id","reason"],"properties":{"run_id":{"type":"string","minLength":1,"maxLength":128},"reason":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"}},"additionalProperties":false}'::jsonb, 'certforge.governance.manage', 2, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 20, 'active', 'unknown'),
('echo.certforge.policy_packs.create', 'Create a versioned tenant policy pack.', 'http', 'http://127.0.0.1:8309/v1/subscriber/policy-packs', 'POST', 'json_body', 'forge',
'{"type":"object","required":["name","version","manifest"],"properties":{"name":{"type":"string"},"version":{"type":"string"},"manifest":{"type":"object"}},"additionalProperties":false}'::jsonb, 'certforge.policy_pack.manage', 2, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 20, 'active', 'unknown'),
('echo.certforge.policy_packs.list', 'List tenant policy packs.', 'http', 'http://127.0.0.1:8309/v1/subscriber/policy-packs', 'GET', 'query', 'forge',
Expand Down
5 changes: 3 additions & 2 deletions scripts/register_certforge_sdk_schemas.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ INSERT INTO certforge_sdk_contract(id, input_schema_json, output_schema_json) VA
('echo.certforge.evidence', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"type":"string"}},"required":["command","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb),
('echo.certforge.findings', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"type":"string"}},"required":["command","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb),
('echo.certforge.governance.get', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"}},"required":["command"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb),
('echo.certforge.governance.reconcile_reservation', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"reason":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"},"run_id":{"type":"string"}},"required":["command","reason","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb),
('echo.certforge.governance.update', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"config":{"additionalProperties":false,"properties":{"allowed_policy_ids":{"items":{"type":"string"},"type":"array"},"customer_managed_signing":{"type":"boolean"},"customer_signing_key_id":{"anyOf":[{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"},{"type":"null"}]},"local_only_execution":{"type":"boolean"},"private_worker_only":{"type":"boolean"},"report_brand_name":{"anyOf":[{"type":"string"},{"type":"null"}]},"report_logo_url":{"anyOf":[{"type":"string"},{"type":"null"}]},"retention_days":{"type":"integer"}},"required":["allowed_policy_ids","retention_days","private_worker_only","report_brand_name","report_logo_url","customer_managed_signing","local_only_execution"],"type":"object"},"expected_version":{"minimum":1.0,"type":"integer"}},"required":["command","config","expected_version"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb),
('echo.certforge.health', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"}},"required":["command"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb),
('echo.certforge.list_runs', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"}},"required":["command"],"type":"object"}'::jsonb, '{"items":{"additionalProperties":true,"type":"object"},"type":"array"}'::jsonb),
Expand Down Expand Up @@ -75,8 +76,8 @@ BEGIN
FROM arcanum_sdk.sdk_capabilities
WHERE id LIKE 'echo.certforge.%'
OR id = 'echo.certification_forge.r5.run_negative_controls';
IF live_count <> 61 THEN
RAISE EXCEPTION 'Certification Forge SDK surface drift: live %, contract 61', live_count;
IF live_count <> 62 THEN
RAISE EXCEPTION 'Certification Forge SDK surface drift: live %, contract 62', live_count;
END IF;
IF EXISTS (
SELECT 1 FROM certforge_sdk_contract c
Expand Down
21 changes: 21 additions & 0 deletions src/echo_certification_forge/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ class RerunRequest(BaseModel):
idempotency_key: str = Field(min_length=8, max_length=128)


class ReleasedReservationReconcileRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
command: str | None = Field(default=None, min_length=1, max_length=64)
reason: str = Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")


class ApiKeyCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
command: str | None = Field(default=None, min_length=1, max_length=64)
Expand Down Expand Up @@ -636,6 +642,21 @@ def cancel_certification(
raise HTTPException(status_code=409, detail="run_not_cancellable") from exc
return project_run(context.store, context.store.get_run(run_id, tenant_id))

@app.post("/v1/subscriber/certifications/{run_id}/reconcile-reservation")
def reconcile_released_reservation(
run_id: str,
request: ReleasedReservationReconcileRequest,
x_tenant_id: str | None = Header(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
principal = subscriber_principal(
x_tenant_id, authorization, Permission.GOVERNANCE_MANAGE,
"certifications.reservations.reconcile",
)
return context.subscribers.reconcile_released_reservation(
principal, run_id=run_id, reason=request.reason
)

@app.get("/v1/certifications/{run_id}/findings")
def get_findings(
run_id: str,
Expand Down
Loading
Loading