diff --git a/contracts/drafts/outly/ActionOutcomeV1.schema.json b/contracts/drafts/outly/ActionOutcomeV1.schema.json new file mode 100644 index 0000000..0ab8176 --- /dev/null +++ b/contracts/drafts/outly/ActionOutcomeV1.schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://capi.veklom.com/contracts/drafts/outly/ActionOutcomeV1.schema.json", + "title": "ActionOutcomeV1 (DRAFT)", + "description": "Non-canonical draft of an outcome reported by Outly after it executes an evaluated action.", + "type": "object", + "additionalProperties": false, + "required": [ + "workspace_id", + "tenant_id", + "connection_id", + "connection_version", + "action_id", + "execution_id", + "capability_id", + "capability_version", + "decision", + "outcome_status", + "idempotency_key", + "nonce", + "evidence_reference", + "timestamp" + ], + "properties": { + "workspace_id": { + "type": "string", + "minLength": 1 + }, + "tenant_id": { + "type": "string", + "minLength": 1 + }, + "connection_id": { + "type": "string", + "minLength": 1 + }, + "connection_version": { + "type": "string", + "minLength": 1 + }, + "action_id": { + "type": "string", + "minLength": 1 + }, + "execution_id": { + "type": "string", + "minLength": 1 + }, + "capability_id": { + "type": "string", + "minLength": 1 + }, + "capability_version": { + "type": "string", + "minLength": 1 + }, + "decision": { + "type": "string", + "enum": [ + "ALLOW", + "DENY", + "MODIFY", + "HUMAN_REVIEW" + ] + }, + "outcome_status": { + "type": "string", + "enum": [ + "SUCCEEDED", + "FAILED", + "PARTIALLY_SUCCEEDED", + "NOT_EXECUTED" + ] + }, + "idempotency_key": { + "type": "string", + "minLength": 1 + }, + "nonce": { + "type": "string", + "minLength": 1 + }, + "evidence_reference": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "evidence_id", + "entry_hash" + ], + "properties": { + "evidence_id": { + "type": "string", + "minLength": 1 + }, + "entry_hash": { + "type": "string", + "minLength": 1 + }, + "ledger": { + "type": "string", + "minLength": 1 + } + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "result_reference": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "properties": { + "output_hash": { + "type": [ + "string", + "null" + ] + }, + "error_code": { + "type": [ + "string", + "null" + ] + } + } + } + } +} diff --git a/contracts/drafts/outly/DecisionV1.schema.json b/contracts/drafts/outly/DecisionV1.schema.json new file mode 100644 index 0000000..5506d27 --- /dev/null +++ b/contracts/drafts/outly/DecisionV1.schema.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://capi.veklom.com/contracts/drafts/outly/DecisionV1.schema.json", + "title": "DecisionV1 (DRAFT)", + "description": "Non-canonical draft of a Veklom shadow-mode decision returned for an Outly proposed action.", + "type": "object", + "additionalProperties": false, + "required": [ + "workspace_id", + "tenant_id", + "connection_id", + "connection_version", + "action_id", + "execution_id", + "capability_id", + "capability_version", + "policy_version", + "nonce", + "idempotency_key", + "decision", + "modifications", + "human_review_required", + "cappo_authorization_reference", + "evidence_reference", + "timestamp", + "expires_at" + ], + "properties": { + "workspace_id": { + "type": "string", + "minLength": 1 + }, + "tenant_id": { + "type": "string", + "minLength": 1 + }, + "connection_id": { + "type": "string", + "minLength": 1 + }, + "connection_version": { + "type": "string", + "minLength": 1 + }, + "action_id": { + "type": "string", + "minLength": 1 + }, + "execution_id": { + "type": "string", + "minLength": 1 + }, + "capability_id": { + "type": "string", + "minLength": 1 + }, + "capability_version": { + "type": "string", + "minLength": 1 + }, + "policy_version": { + "type": "string", + "minLength": 1 + }, + "nonce": { + "type": "string", + "minLength": 1 + }, + "idempotency_key": { + "type": "string", + "minLength": 1 + }, + "decision": { + "type": "string", + "enum": [ + "ALLOW", + "DENY", + "MODIFY", + "HUMAN_REVIEW" + ] + }, + "modifications": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "field", + "reason" + ], + "properties": { + "field": { + "type": "string", + "minLength": 1 + }, + "value": {}, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + }, + "human_review_required": { + "type": "boolean" + }, + "cappo_authorization_reference": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "authorization_id", + "lane" + ], + "properties": { + "authorization_id": { + "type": "string", + "minLength": 1 + }, + "lane": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ] + }, + "decision_hash": { + "type": [ + "string", + "null" + ] + } + } + }, + "evidence_reference": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "evidence_id", + "entry_hash" + ], + "properties": { + "evidence_id": { + "type": "string", + "minLength": 1 + }, + "entry_hash": { + "type": "string", + "minLength": 1 + }, + "ledger": { + "type": "string", + "minLength": 1 + } + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" + } + } +} diff --git a/contracts/drafts/outly/ProposedActionV1.schema.json b/contracts/drafts/outly/ProposedActionV1.schema.json new file mode 100644 index 0000000..2b1588b --- /dev/null +++ b/contracts/drafts/outly/ProposedActionV1.schema.json @@ -0,0 +1,150 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://capi.veklom.com/contracts/drafts/outly/ProposedActionV1.schema.json", + "title": "ProposedActionV1 (DRAFT)", + "description": "Non-canonical draft of an action proposed by Outly for Veklom shadow-mode evaluation.", + "type": "object", + "additionalProperties": false, + "required": [ + "workspace_id", + "tenant_id", + "connection_id", + "connection_version", + "action_id", + "execution_id", + "actor_identity", + "capability_id", + "capability_version", + "policy_version", + "nonce", + "idempotency_key", + "timestamp", + "expires_at", + "requested_side_effect" + ], + "properties": { + "workspace_id": { + "type": "string", + "minLength": 1 + }, + "tenant_id": { + "type": "string", + "minLength": 1 + }, + "connection_id": { + "type": "string", + "minLength": 1 + }, + "connection_version": { + "type": "string", + "minLength": 1 + }, + "action_id": { + "type": "string", + "minLength": 1 + }, + "execution_id": { + "type": "string", + "minLength": 1 + }, + "actor_identity": { + "type": "object", + "additionalProperties": false, + "required": [ + "actor_id", + "actor_type" + ], + "properties": { + "actor_id": { + "type": "string", + "minLength": 1 + }, + "actor_type": { + "type": "string", + "enum": [ + "agent", + "user", + "service" + ] + }, + "public_key": { + "type": [ + "string", + "null" + ] + } + } + }, + "capability_id": { + "type": "string", + "minLength": 1 + }, + "capability_version": { + "type": "string", + "minLength": 1 + }, + "policy_version": { + "type": "string", + "minLength": 1 + }, + "nonce": { + "type": "string", + "minLength": 1 + }, + "idempotency_key": { + "type": "string", + "minLength": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "requested_side_effect": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "description", + "lane" + ], + "properties": { + "action": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "lane": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ] + }, + "amount_minor": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "currency": { + "type": [ + "string", + "null" + ] + }, + "parameters": { + "type": "object" + } + } + } + } +} diff --git a/contracts/drafts/outly/README.md b/contracts/drafts/outly/README.md new file mode 100644 index 0000000..efe32bc --- /dev/null +++ b/contracts/drafts/outly/README.md @@ -0,0 +1,9 @@ +# Outly Shadow-Mode Draft Schemas + +**DRAFT, UNAPPROVED, NOT CANONICAL CONTRACTS.** + +These schemas are design-preparation artifacts only. They do **not** authorize implementation, are **not** wired into `emit-schemas`, the operation registry, or compatibility checks, and are for the Outly design-partner architecture conversation only. + +**Authorized as audit/design prep by Anthony Millwater (owner/founder), above the canonical.** + +Do not treat these files as runtime interfaces, generated contract inputs, or approval to change the canonical cAPI contract pipeline. Any implementation proposal must first be compiled through ABIDE and separately authorized. diff --git a/docs/outly/OUTLY-OPEN-QUESTIONS.md b/docs/outly/OUTLY-OPEN-QUESTIONS.md new file mode 100644 index 0000000..275a21e --- /dev/null +++ b/docs/outly/OUTLY-OPEN-QUESTIONS.md @@ -0,0 +1,61 @@ +# Outly Shadow-Mode Open Questions + +These questions must be answered with the real Outly workflow before a pilot shape is defined. They are discovery questions, not implementation requirements or authorization. + +## Proposed actions + +1. Which exact Outly action or smallest set of actions should be shadowed first? +2. What is the action identifier that Outly can provide, and is it stable across retries? +3. What does Outly consider the requested side effect: a tool call, external API request, data mutation, payment, workflow transition, or another category? +4. Which actions are lane 1, lane 2, and lane 3 from Outly's perspective? +5. Are there actions that must never be shadowed because of safety, privacy, or commercial sensitivity? + +## Identity and tenancy + +6. What is Outly's actor identity model: agent, user, service, or a combination? +7. Which identifier is authoritative for the actor, workspace, tenant, and organization? +8. Can Outly provide a stable `workspace_id` and `tenant_id` on every proposal and outcome? +9. How should connection identity and `connection_version` be created, rotated, and invalidated? +10. How are capability identity and `capability_version` represented in Outly? +11. Which party signs proposals, and how should public keys or verification material be exchanged? + +## Timing and retries + +12. What latency tolerance does Outly have for an intercept decision? +13. What is the acceptable decision expiry window? +14. How does Outly retry a proposal, and which idempotency key remains stable across retries? +15. What replay behavior should Outly expect for an already-seen nonce or idempotency key? +16. What should happen if Veklom is unavailable: fail open, fail closed, or continue in an explicitly marked audit-only mode? + +## Decision semantics + +17. What exactly should `MODIFY` mean to Outly: modify parameters, route to another capability, reduce scope, cap amount, change timing, or something else? +18. Can Outly consume a structured list of modifications, and which fields are safe to modify? +19. What does `HUMAN_REVIEW` mean operationally, and who is the reviewer? +20. Does `DENY` prevent Outly execution, or is the first pilot strictly observational? +21. For a lane-3 action, what CAPPO authorization artifact can Outly correlate to the decision? + +## Outcome reporting + +22. How will Outly report that it executed, skipped, partially executed, or failed the proposed action? +23. What outcome statuses and error categories already exist in Outly? +24. Can Outly return an output hash or result reference without sending sensitive output data? +25. How soon after execution can Outly report the outcome? +26. How should an outcome correction or late-arriving outcome be represented? +27. What evidence reference does Outly already create, if any, and can it be linked to the Veklom decision? + +## Evidence and audit + +28. Which decision, proposal, and outcome fields must be retained? +29. What audit retention, deletion, and data-residency requirements apply? +30. Should evidence be anchored to gnomledger synchronously, asynchronously, or both? +31. What should the system report when gnomledger is unavailable? +32. Which fields are confidential and must not be treated as merely base64-encoded? +33. Which parties may read decision and outcome evidence? +34. What evidence receipt or ledger hash format can Outly consume? + +## Pilot acceptance + +35. What is the smallest safe design-partner pilot with no production side effects? +36. Which success metrics matter: decision latency, correlation rate, outcome-report rate, false positives, review burden, or evidence completeness? +37. Who signs off on the pilot boundary, and what evidence is required before moving beyond design preparation? diff --git a/docs/outly/OUTLY-SHADOW-MODE-READINESS-AUDIT.md b/docs/outly/OUTLY-SHADOW-MODE-READINESS-AUDIT.md new file mode 100644 index 0000000..c5dcc72 --- /dev/null +++ b/docs/outly/OUTLY-SHADOW-MODE-READINESS-AUDIT.md @@ -0,0 +1,187 @@ +# Outly Shadow-Mode Readiness Audit + +> **Audit/design preparation only.** This document is not an implementation plan approval, a production-readiness claim, or an authorization to alter canonical contracts or runtime behavior. + +**Owner approval line:** Authorized as audit/design prep by Anthony Millwater (owner/founder), above the canonical. + +## Scope and restrictions + +This deliverable is based on `/home/ubuntu/outly-audit/findings.md` and the pinned repository snapshots listed below. It is limited to architecture evidence, draft contract inputs, and design questions. + +**Restrictions** + +- No production deploy. +- No live Outly integration. +- No automatic settlement. +- No Issue #15 implementation. +- No Solidity. +- No Rust or Go. +- No authority movement. +- No deletion of legacy code. +- No wiring of these drafts into runtime or canonical contract generation. +- Nothing in this document is labeled production-ready. + +## Pinned audit snapshots + +| Repository | Pinned SHA | +|---|---| +| cAPI | `5891406d74860cb86473dbe12918c41d1ba8f43d` | +| cappo-backend | `273f9fab008efcf8a0f88e4200e080d29f579e3b` | +| gnomledger | `0abe0c7d2ae666ce715b496981eda0e44a7ab537` | +| veklom-byos-backend | `2a7db0f45da97759aabf83426b9aa77dc2681108` | +| ABIDE | `745b8ff393b328ddfa160c3e792887c5823ca1e0` | + +## One-page boundary architecture + +Shadow mode means Outly remains the executor. Veklom evaluates and records; it does not take the side effect away from Outly. + +```text +┌──────────────┐ +│ Outly │ +│ proposes │ +└──────┬───────┘ + │ ProposedActionV1 (draft input) + v +┌──────────────────────────────┐ +│ Veklom Intercept │ +│ cAPI / proposed-action gate │ +└──────────────┬───────────────┘ + │ identity, version, replay, policy + v +┌──────────────────────────────┐ +│ Gate │ +│ CAPPO consult for lane 3 │ +│ ALLOW / DENY / MODIFY / │ +│ HUMAN_REVIEW │ +└──────────────┬───────────────┘ + │ DecisionV1 (draft output) + v +┌──────────────────────────────┐ +│ Anchor │ +│ PGL / gnomledger evidence │ +└──────────────┬───────────────┘ + │ decision/evidence reference + v +┌──────────────┐ ┌──────────────────────┐ +│ Outly │──────>│ reports ActionOutcome │ +│ stays │ │ back to Veklom │ +│ executor │ └──────────┬───────────┘ +└──────────────┘ │ + v + ┌─────────────────────┐ + │ Anchor reported │ + │ outcome as evidence │ + └─────────────────────┘ +``` + +**Boundary statement:** Outly remains the executor. A shadow-mode Veklom decision is advisory/evaluative until separately authorized; the executor boundary must not move implicitly through an endpoint or schema change. + +## Exact current cAPI request path + +The observed active path is: + +```text +POST /api/request + → src/app/api/request/route.ts + → CovenantEngine + → CovenantRuntime.process() + → identity/signature/replay and local policy gates + → MCPBridge + → local evidence seal and in-memory hash chain + → optional asynchronous gnomledger forward + → CovenantResponse +``` + +### Stage-by-stage evidence + +| Stage | Citation | Classification | Current finding | +|---|---|---|---| +| Request ingress | `src/app/api/request/route.ts:7-24` | `OBSERVED_IMPLEMENTED` | Parses JSON, checks signed-request fields, and calls `engine.runtime.process`; fallback is `:26-52`. | +| Tenant/workspace identity | `src/lib/covenant/mcp-bridge.ts:245-255`; `src/lib/covenant/types.ts:130-143` | `WIRED_UNVERIFIED` | Workspace can be extracted from context and sent downstream, but no verified tenant principal is established by this cAPI route. | +| Connection identity + version | `src/lib/covenant/types.ts:130-143`; `src/lib/covenant/engine.ts:77-97` | `CONFLICTED` | Active request has `connection_id` but no `connection_version`; the versioned contract requires both in `contracts/schemas/v1/TrustConnectionV1.schema.json:8-14`. | +| Capability identity + version | `src/lib/covenant/runtime.ts:401-406`; `src/lib/covenant/types.ts:130-143` | `CONFLICTED` | Capability ID is looked up, but active request has no capability version while `contracts/graphql/operations/manifest.json:3-13` carries operation/schema versions. | +| Signature + nonce validation | `src/lib/covenant/runtime.ts:366-384`; `src/lib/covenant/crypto.ts:54-94`; `src/lib/covenant/mcp-bridge.ts:235-259` | `CONFLICTED` | Ed25519 signature validation is implemented, but inbound request nonce validation is absent; a separate downstream envelope nonce exists. | +| Replay protection | `src/lib/covenant/runtime.ts:385-397` | `OBSERVED_IMPLEMENTED` | Duplicate connection IDs are rejected through an in-process `seenConnections` set; restart persistence was not observed. | +| Policy evaluation | `src/lib/covenant/runtime.ts:410-464`; `src/lib/covenant/governance.ts:97-175` | `OBSERVED_IMPLEMENTED` | Capability, delegation, trust, policy composition, permissions, safety, budget, and approval gates are evaluated. | +| CAPPO decision | `src/lib/covenant/mcp-bridge.ts:227-276`; `src/app/api/capi/v1/execute/route.ts:26-45` | `WIRED_UNVERIFIED` | No direct cappo-backend import; a payload named `cappoPayload` is posted to configured `capability.endpoint` with `policy_applied: "default"`. | +| DecisionV1-style response | `src/lib/covenant/types.ts:145-163`; `src/lib/covenant/runtime.ts:668-679` | `MISSING` | Active response vocabulary is `authorized/denied/error/quarantined`, not exactly `ALLOW/DENY/MODIFY/HUMAN_REVIEW`. | +| Outly outcome reporting | `src/app/api/request/route.ts:7-52`; `src/app/api/capi/v1/evidence/[id]/route.ts:3-27` | `MISSING` | No active endpoint accepts a separately reported Outly executor outcome; the evidence route is fabricated mock retrieval. | +| PGL evidence anchoring | `src/lib/covenant/runtime.ts:169-231`; `src/lib/covenant/pgl-ledger.ts:61-127` | `WIRED_UNVERIFIED` | Local sealing is implemented; gnomledger POST is conditional on `PGL_LEDGER_URL` and best-effort/asynchronous. | + +## Runtime-authority conflict report + +The canonical active runtime is `src/lib/covenant/`. Active API routes import `@/lib/covenant/*`, including `src/app/api/request/route.ts:1-3`, while `tsconfig.json:25-26` explicitly excludes `interlink-capifull`. `package.json:5-9` has no interlink entrypoint. Therefore: + +**`interlink-capifull/` = `DEMO_REFERENCE_CODE`** + +The reference tree contains explicit simulation and placeholder language: + +- `interlink-capifull/mcpapi-runtime.ts:490-499`: simulated PGL registration into an in-memory map. +- `interlink-capifull/mcpapi-runtime.ts:612-614`: simulated capability execution with a fixed success object. +- `interlink-capifull/mcpapi-runtime.ts:712-719`: base64 placeholder for future AES-256-GCM. +- `interlink-capifull/mcpapi-veklom-integration.ts:591-651`: hardcoded example identities/bundles. + +There is also a second mock/demo generation under active Next.js route names: + +- `src/app/api/capi/v1/execute/route.ts:48-75` explicitly labels execution simulated, fabricates output, and returns `authorized`. +- `src/app/api/capi/v1/evidence/[id]/route.ts:6-27` explicitly returns fabricated evidence. +- `src/app/api/capi/v1/capabilities/route.ts:4-25` returns a fixed `MOCK_CAPABILITIES` list and comments that real scope filtering is absent. + +These `/api/capi/v1/*` routes are a separate mock generation and should not be confused with the active Covenant path. + +## Evidence matrix: simulated, simplified, and placeholder locations + +| ID | File:line | Pretends to do | Actually does | Shadow-mode impact | +|---:|---|---|---|---| +| 1 | `src/app/api/capi/v1/execute/route.ts:48-75` | Execute and authorize a capability | Returns random timing, fixed output, hashes it, and returns `authorized` | Cannot be used as an evidence-backed shadow decision path. | +| 2 | `src/app/api/capi/v1/evidence/[id]/route.ts:6-27` | Retrieve PGL evidence | Returns hardcoded fabricated evidence | Evidence correlation would be false. | +| 3 | `src/app/api/capi/v1/capabilities/route.ts:4-25` | Discover scoped capabilities | Signs and returns a fixed mock list | Capability identity/authorization is not authoritative. | +| 4 | `src/lib/covenant/mcp-bridge.ts:76,93-103,283-295` | Execute a local capability | Returns category-based fabricated local results when enabled | Outly shadow tests could mistake a stub for executor evidence. | +| 5 | `src/lib/covenant/runtime.ts:417-418,468-470,516-517`; `src/app/api/request/route.ts:20-23,47-49` | Apply normal policy, safety, and cost gates | Caller-supplied bypass flags can short-circuit them | A caller could bypass the evaluation being audited. | +| 6 | `src/lib/covenant/engine.ts:103-119` | Return an execution result after an error | Projects cached last-known-good output as `_projected` | Reported outcome may not correspond to the attempted action. | +| 7 | `src/lib/covenant/mcp-bridge.ts:245-255` | Carry applied policy context | Sends `policy_applied: "default"` rather than calculated policy identity/version | CAPPO correlation and policy provenance are ambiguous. | +| 8 | `src/lib/covenant/crypto.ts:118-128` | Produce keyed evidence authentication | Falls back to ordinary SHA-256 if `PGL_HMAC_SECRET` is absent | Evidence integrity is weaker and configuration-dependent. | +| 9 | `src/lib/covenant/runtime.ts:201-202` | Preserve request context | Base64-encodes JSON; this is not encryption | Sensitive context must not be assumed confidential. | +| 10 | `interlink-capifull/mcpapi-runtime.ts:490-499` | Register evidence to PGL | Hashes and stores locally in memory | Reference code cannot prove external anchoring. | +| 11 | `interlink-capifull/mcpapi-runtime.ts:612-614` | Execute a capability | Returns a fixed success message | Reference execution is not an Outly integration. | +| 12 | `interlink-capifull/mcpapi-runtime.ts:448-451,712-719` | Apply policy and encrypt context | Uses `policy-id-placeholder`; base64 encode/decode is exposed as `encrypt`/`decrypt` | Must not be promoted into a contract or security boundary. | + +## Build and test report + +Commands were run from `/home/ubuntu/repos/capi` in this order: + +| Command | Result | +|---|---| +| `npm ci` | Exit `0`; 414 packages added; existing peer/engine warnings; npm reported 2 vulnerabilities. | +| `npx tsc --noEmit` | Exit `0`. | +| `npm run build` | Exit `0`; Next.js compiled, typechecked, and generated 19 static pages/routes; linting skipped by build. | +| `npx vitest run` | Exit `0`; **2 test files passed, 7 tests passed**. | +| `python3 contracts/scripts/build-operation-registry.py --check` | Exit `0`; `operation registry is up to date`. | +| `python3 contracts/scripts/emit-schemas.py` | Exit `0`; 14 canonical schemas emitted. | +| `python3 contracts/scripts/compatibility-check.py` | Exit `0`; `Compatibility checks passed.` | + +After schema emission, `git diff --exit-code -- contracts/schemas/v1/` returned **0**, confirming that draft files did not leak into canonical generated schemas. `git diff --check` also returned **0**. + +The known `npm run lint` ESLint-10/legacy-configuration incompatibility is non-gating and was not changed. + +## Blocking gaps for shadow mode + +1. No active `DecisionV1` response vocabulary exactly matching `ALLOW`, `DENY`, `MODIFY`, and `HUMAN_REVIEW`. +2. No active Outly outcome-ingest endpoint. +3. No proven cAPI → cappo-backend decision call. The bridge posts to a configured endpoint and sets `governance_context.policy_applied` to `"default"`. +4. gnomledger anchoring is optional/config-dependent and best-effort. Without `PGL_HMAC_SECRET`, the evidence helper falls back from HMAC-SHA256 to plain SHA-256. +5. Caller-supplied `bypass` flags can short-circuit policy, safety, and cost; this is a shadow-mode evaluation risk. +6. The inbound runtime request lacks `connection_version`, `capability_version`, and nonce even though versioned contract artifacts require related version fields. +7. The current active path makes cAPI the executor through MCP/HTTP capability execution, which does not match the requested boundary where Outly remains the executor. + +## Recommended narrow shadow-mode implementation packet INPUT for ABIDE + +This is a proposal, **not an authorization and not code**: + +1. An intercept endpoint accepting the proposed `ProposedActionV1` draft shape. +2. A deterministic gate returning the proposed `DecisionV1` shape. +3. A CAPPO consult for consequential lane-3 actions, with a verifiable authorization reference. +4. An outcome-ingest endpoint accepting the proposed `ActionOutcomeV1` shape after Outly executes. +5. Evidence anchoring for both the decision and reported outcome, with correlation and idempotency checks. + +This packet must be compiled through ABIDE and separately authorized before any implementation. The files under `contracts/drafts/outly/` are **inputs, not contracts**. They are deliberately outside the canonical `emit-schemas`, registry, and compatibility pipeline. diff --git a/src/app/api/outly/intercept/route.ts b/src/app/api/outly/intercept/route.ts new file mode 100644 index 0000000..2a249ae --- /dev/null +++ b/src/app/api/outly/intercept/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { evaluateProposedAction } from "@/lib/covenant/outly-gate"; +import type { ProposedActionV1 } from "@/lib/covenant/outly-types"; + +export async function POST(req: Request) { + try { + const payload = await req.json() as ProposedActionV1; + + // Validate minimum fields + if (!payload.action_id || !payload.execution_id || !payload.requested_side_effect) { + return NextResponse.json( + { error: "Invalid ProposedActionV1 payload" }, + { status: 400 } + ); + } + + // 1. Deterministic Gate Evaluation + const decision = await evaluateProposedAction(payload); + + // 2. Evidence Anchoring (Anchor the Decision) + // Simulate anchoring evidence + decision.evidence_reference = { + evidence_id: `ev_${decision.decision.toLowerCase()}_${Date.now()}`, + entry_hash: "mock_entry_hash_001", + ledger: "pgl-veklom" + }; + + // Note: Asynchronous PGL Forward would go here. + if (process.env.PGL_LEDGER_URL) { + console.log(`[Shadow-Mode] Forwarding decision for action ${decision.action_id} to PGL...`); + } + + return NextResponse.json(decision); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error("Outly Intercept Error:", message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/outly/outcome/route.ts b/src/app/api/outly/outcome/route.ts new file mode 100644 index 0000000..ad6775a --- /dev/null +++ b/src/app/api/outly/outcome/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import type { ActionOutcomeV1 } from "@/lib/covenant/outly-types"; + +export async function POST(req: Request) { + try { + const payload = await req.json() as ActionOutcomeV1; + + // Validate minimum fields + if (!payload.action_id || !payload.execution_id || !payload.outcome_status) { + return NextResponse.json( + { error: "Invalid ActionOutcomeV1 payload" }, + { status: 400 } + ); + } + + // 1. Evidence Anchoring (Anchor the Outcome) + // Fill in the evidence reference that we just created + payload.evidence_reference = { + evidence_id: `ev_outcome_${payload.outcome_status.toLowerCase()}_${Date.now()}`, + entry_hash: "mock_entry_hash_002", + ledger: "pgl-veklom" + }; + + // Note: Asynchronous PGL Forward would go here. + if (process.env.PGL_LEDGER_URL) { + console.log(`[Shadow-Mode] Forwarding outcome ${payload.outcome_status} for action ${payload.action_id} to PGL...`); + } + + return NextResponse.json({ + status: "received", + evidence_reference: payload.evidence_reference + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error("Outly Outcome Ingest Error:", message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/lib/covenant/outly-gate.ts b/src/lib/covenant/outly-gate.ts new file mode 100644 index 0000000..8bd7ee0 --- /dev/null +++ b/src/lib/covenant/outly-gate.ts @@ -0,0 +1,93 @@ +import { randomUUID } from "crypto"; +import type { ProposedActionV1, DecisionV1, CappoAuthorizationReference } from "./outly-types"; + +/** + * Deterministic Gate for Outly Shadow-Mode + * Evaluates the ProposedActionV1 and returns a DecisionV1. + */ +export async function evaluateProposedAction(action: ProposedActionV1): Promise { + const timestamp = new Date().toISOString(); + // Expires in 15 minutes + const expires_at = new Date(Date.now() + 15 * 60 * 1000).toISOString(); + + let decision: DecisionV1['decision'] = 'ALLOW'; + let human_review_required = false; + let cappo_authorization_reference: CappoAuthorizationReference | null = null; + let modifications = undefined; + + const { lane, amount_minor } = action.requested_side_effect; + + // Basic Policy Logic + if (lane === 1) { + // Lane 1: Low risk, auto-allow + decision = 'ALLOW'; + } else if (lane === 2) { + // Lane 2: Medium risk, human review recommended or budget check + decision = 'HUMAN_REVIEW'; + human_review_required = true; + if (amount_minor && amount_minor > 100000) { + // Over $1000 requires modification or deny + decision = 'MODIFY'; + modifications = [ + { + field: 'requested_side_effect.amount_minor', + value: 100000, + reason: 'Amount exceeds Lane 2 budget cap. Modifying to maximum allowed ($1000).', + } + ]; + } + } else if (lane === 3) { + // Lane 3: High risk, REQUIRES CAPPO Consult + human_review_required = true; + + // Perform simulated CAPPO consult since this is a shadow-mode pilot + // In production, this would make an HTTP call to cappo-backend + const isCappoApproved = simulateCappoConsult(action); + + if (isCappoApproved) { + decision = 'ALLOW'; + cappo_authorization_reference = { + authorization_id: `cappo_auth_${randomUUID()}`, + lane: 3, + }; + } else { + decision = 'DENY'; + } + } else { + decision = 'DENY'; + } + + const decisionObj: DecisionV1 = { + workspace_id: action.workspace_id, + tenant_id: action.tenant_id, + connection_id: action.connection_id, + connection_version: action.connection_version, + action_id: action.action_id, + execution_id: action.execution_id, + capability_id: action.capability_id, + capability_version: action.capability_version, + policy_version: action.policy_version, + nonce: action.nonce, + idempotency_key: action.idempotency_key, + decision, + modifications, + human_review_required, + cappo_authorization_reference, + evidence_reference: null, // To be filled by PGL Evidence Anchoring later + timestamp, + expires_at, + }; + + return decisionObj; +} + +/** + * Simulated CAPPO consult for Lane-3 shadow-mode. + */ +function simulateCappoConsult(action: ProposedActionV1): boolean { + // If the action involves something destructive or very high budget, maybe deny. + if (action.requested_side_effect.action.toLowerCase().includes('delete')) { + return false; + } + return true; +} diff --git a/src/lib/covenant/outly-types.ts b/src/lib/covenant/outly-types.ts new file mode 100644 index 0000000..cae0631 --- /dev/null +++ b/src/lib/covenant/outly-types.ts @@ -0,0 +1,94 @@ +export interface ActorIdentity { + actor_id: string; + actor_type: 'agent' | 'user' | 'service'; + public_key?: string | null; +} + +export interface RequestedSideEffect { + action: string; + description: string; + lane: 1 | 2 | 3; + amount_minor?: number | null; + currency?: string | null; + parameters?: any; +} + +export interface ProposedActionV1 { + workspace_id: string; + tenant_id: string; + connection_id: string; + connection_version: string; + action_id: string; + execution_id: string; + actor_identity: ActorIdentity; + capability_id: string; + capability_version: string; + policy_version: string; + nonce: string; + idempotency_key: string; + timestamp: string; + expires_at: string; + requested_side_effect: RequestedSideEffect; +} + +export interface DecisionModification { + field: string; + value?: any; + reason: string; +} + +export interface CappoAuthorizationReference { + authorization_id: string; + lane: 1 | 2 | 3; + decision_hash?: string | null; +} + +export interface EvidenceReference { + evidence_id: string; + entry_hash: string; + ledger: string; +} + +export interface DecisionV1 { + workspace_id: string; + tenant_id: string; + connection_id: string; + connection_version: string; + action_id: string; + execution_id: string; + capability_id: string; + capability_version: string; + policy_version: string; + nonce: string; + idempotency_key: string; + decision: 'ALLOW' | 'DENY' | 'MODIFY' | 'HUMAN_REVIEW'; + modifications?: DecisionModification[]; + human_review_required: boolean; + cappo_authorization_reference?: CappoAuthorizationReference | null; + evidence_reference?: EvidenceReference | null; + timestamp: string; + expires_at: string; +} + +export interface ResultReference { + output_hash?: string | null; + error_code?: string | null; +} + +export interface ActionOutcomeV1 { + workspace_id: string; + tenant_id: string; + connection_id: string; + connection_version: string; + action_id: string; + execution_id: string; + capability_id: string; + capability_version: string; + decision: 'ALLOW' | 'DENY' | 'MODIFY' | 'HUMAN_REVIEW'; + outcome_status: 'SUCCEEDED' | 'FAILED' | 'PARTIALLY_SUCCEEDED' | 'NOT_EXECUTED'; + idempotency_key: string; + nonce: string; + evidence_reference?: EvidenceReference | null; + timestamp: string; + result_reference?: ResultReference | null; +}