feat(agent-access): capability catalog — who can do what (seed v1) #566

Merged
pdurlej merged 2 commits from ollama/dziadek-agent-capability-catalog into main 2026-06-02 15:24:51 +02:00
Collaborator

Agent Capability Catalog — seed v1

A machine-readable catalog of what each cousin agent can do. Agents consult this before delegating work across cousins.

Extracted from claude's full Agent Access Plane Spec Kit — only the immediately useful 20% (the catalog itself), leaving the 1000-line Spec Kit and stub code in the original branch for future reference.

Author: dziadek (DeepSeek-v4-Pro / ollama), based on claude's draft (2026-05-17)

What's in the box (4 files, ~10KB)

policies/agent-capabilities.yaml — 7 seed capabilities

Capability Who Audit TTL
forgejo-pat-read claude, codex, glm, deepseek low 24h
forgejo-pr-write claude, codex, deepseek, glm low 8h
ssh-rs2000-platform-host-agent codex only high 1-4h
ssh-vps1000-iskra-readonly codex, claude high 30min-2h
infisical-secrets-read-scoped codex, claude medium 15min-1h
deep-review-full-repo deepseek only medium 8h
break-glass-full-access operator-only critical 10-15min

schema/agent-capability.schema.json

JSON Schema Draft 2020-12. Validates the catalog structure.

agent_access/_types.py

Python dataclasses: Capability, CapabilityBacking, AuditPolicy, Session, AuditEntry. Includes allows_cousin() method for runtime checks.

Why this is useful NOW

  1. Cross-cousin delegation: Before codex delegates to claude, it can check the catalog: "does claude have forgejo-pr-write?"
  2. Single source of truth: Answers "what can each model do?" without ad-hoc conventions or operator mediation
  3. Complements Wake Bus (#565) and Job Bundle (#564): Wake Bus handles WHEN agents coordinate; Job Bundle handles WHAT they pass; Capability Catalog handles WHO can do WHAT

What this does NOT do

  • Does NOT implement runtime credential delivery (future platformctl access)
  • Does NOT include session lifecycle implementation
  • Does NOT replace Infisical as secrets source of truth

Role: deep-reviewer
Refs: #76 (Agent Access Plane parent), ADR-0003

## Agent Capability Catalog — seed v1 A machine-readable catalog of what each cousin agent can do. Agents consult this before delegating work across cousins. **Extracted from claude's full Agent Access Plane Spec Kit** — only the immediately useful 20% (the catalog itself), leaving the 1000-line Spec Kit and stub code in the original branch for future reference. **Author:** dziadek (DeepSeek-v4-Pro / ollama), based on claude's draft (2026-05-17) ## What's in the box (4 files, ~10KB) ### `policies/agent-capabilities.yaml` — 7 seed capabilities | Capability | Who | Audit | TTL | |---|---|---|---| | `forgejo-pat-read` | claude, codex, glm, deepseek | low | 24h | | `forgejo-pr-write` | claude, codex, deepseek, glm | low | 8h | | `ssh-rs2000-platform-host-agent` | codex only | high | 1-4h | | `ssh-vps1000-iskra-readonly` | codex, claude | high | 30min-2h | | `infisical-secrets-read-scoped` | codex, claude | medium | 15min-1h | | `deep-review-full-repo` | deepseek only | medium | 8h | | `break-glass-full-access` | operator-only | critical | 10-15min | ### `schema/agent-capability.schema.json` JSON Schema Draft 2020-12. Validates the catalog structure. ### `agent_access/_types.py` Python dataclasses: `Capability`, `CapabilityBacking`, `AuditPolicy`, `Session`, `AuditEntry`. Includes `allows_cousin()` method for runtime checks. ## Why this is useful NOW 1. **Cross-cousin delegation:** Before codex delegates to claude, it can check the catalog: "does claude have forgejo-pr-write?" 2. **Single source of truth:** Answers "what can each model do?" without ad-hoc conventions or operator mediation 3. **Complements Wake Bus (#565) and Job Bundle (#564):** Wake Bus handles WHEN agents coordinate; Job Bundle handles WHAT they pass; Capability Catalog handles WHO can do WHAT ## What this does NOT do - Does NOT implement runtime credential delivery (future `platformctl access`) - Does NOT include session lifecycle implementation - Does NOT replace Infisical as secrets source of truth **Role:** deep-reviewer **Refs:** #76 (Agent Access Plane parent), ADR-0003
feat(agent-access): capability catalog — who can do what (seed v1)
Some checks failed
canary-required / collect-diff (pull_request) Successful in 5s
patchwarden-client-dry-run / collect-diff (pull_request) Successful in 4s
platformctl plan / auto-apply scope (pull_request) Successful in 23s
python-ci / Python 3.11 (pull_request) Successful in 44s
python-ci / Python 3.12 (pull_request) Successful in 43s
python-ci / Python 3.13 (pull_request) Successful in 43s
canary-required / canary (pull_request) Successful in 15s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 21s
patchwarden-client-dry-run / dry-run (pull_request) Successful in 23s
base-is-main / guard (pull_request) Successful in 1s
patchwarden-pr-sanity / collect-diff (pull_request) Successful in 5s
patchwarden-pr-sanity / sanity (pull_request) Failing after 3m17s
d0e992235b
Extracted from claude's full Agent Access Plane Spec Kit — only the
immediately useful 20%: a machine-readable catalog of cousin capabilities.

Agents consult this before delegating work across cousins:
"Can codex SSH to RS2000? Can deepseek write PRs?"

4 files, ~10KB:
- policies/agent-capabilities.yaml — 7 seed capabilities:
  forgejo-pat-read, forgejo-pr-write,
  ssh-rs2000, ssh-vps1000-iskra-readonly,
  infisical-secrets-read-scoped, deep-review-full-repo,
  break-glass-full-access
- schema/agent-capability.schema.json — JSON Schema 2020-12
- agent_access/_types.py — Capability, Session, AuditEntry dataclasses
- agent_access/__init__.py — package marker

NOT included (left in claude's branch):
- 1000 lines of Spec Kit (docs/specs/agent-access-plane-v0/)
- catalog.py stubs (TODO(codex) markers)
- xfail test scaffolds

Originally drafted by claude (2026-05-17).
Refreshed + extended by dziadek/DeepSeek-v4-Pro (2026-05-28).

Refs: #76, ADR-0003
codex changed title from feat(agent-access): capability catalog — who can do what (seed v1) to WIP: feat(agent-access): capability catalog — who can do what (seed v1) 2026-05-28 14:56:45 +02:00
Collaborator

Codex source-branch salvage appendix (1/2)

Source branch: claude/feat-agent-access-plane
Attached to: ollama/dziadek-agent-capability-catalog / PR #566

Preserves the full Agent Access Plane Spec Kit and stub code omitted from the narrowed capability-catalog PR.

This is archival/reference material copied before deleting stale source branches. It is not an approval to merge the old design/code as-is.

control-plane/platformctl/agent_access/catalog.py
"""Capability catalog loader + validator — Slice (a) scaffold.

Per docs/specs/agent-access-plane-v0/03-tasks.md tasks a.3, a.4.

This is pre-impl scaffold. Codex picks up Slice (a) and fills in:
- yaml.safe_load with structured error reporting
- JSONSchema validation against schema/agent-capability.schema.json
- lookup_capability with cousin ACL check
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from ._types import (
    AuditLevel,
    BackingType,
    Capability,
    CapabilityBacking,
)


class CatalogError(Exception):
    """Raised on catalog load/validation failures."""


def load_catalog(path: str | Path) -> list[Capability]:
    """Load and parse the capability catalog YAML.

    Args:
        path: filesystem path to catalog YAML (typically policies/agent-capabilities.yaml).

    Returns:
        List of Capability dataclasses, one per catalog entry.

    Raises:
        CatalogError: on parse failure, schema violation, or semantic error
                      (e.g., cousin name in both allowed AND forbidden).

    TODO(codex Slice a): implement with yaml.safe_load + JSONSchema validation +
                         semantic checks per 03-tasks.md task a.3.
    """
    raise NotImplementedError("Slice (a) — codex pickup; see 03-tasks.md task a.3")


def validate_catalog(path: str | Path) -> list[str]:
    """Validate the capability catalog without loading it into runtime memory.

    Args:
        path: filesystem path to catalog YAML.

    Returns:
        List of validation error messages. Empty list means catalog is valid.

    TODO(codex Slice a): implement per 03-tasks.md task a.4. CLI exit code:
                         0 for valid (empty list), 1 for invalid.
    """
    raise NotImplementedError("Slice (a) — codex pickup; see 03-tasks.md task a.4")


def lookup_capability(
    cap_id: str, cousin: str, catalog: list[Capability]
) -> Capability:
    """Find capability by ID, verifying cousin is in allowed list and not forbidden.

    Args:
        cap_id: capability ID (matches Capability.id).
        cousin: cousin name (claude/codex/glm/...).
        catalog: loaded catalog.

    Returns:
        Capability dataclass.

    Raises:
        CatalogError: cap_id not found, cousin in cousin_forbidden, or cousin
                      not in cousin_allowed.

    TODO(codex Slice a): implement per 03-tasks.md task a.3. Per Constitution P7
                         and FR-7, refusal must be loud + audit-logged.
    """
    raise NotImplementedError("Slice (a) — codex pickup; see 03-tasks.md task a.3")


def _parse_capability_dict(raw: dict[str, Any]) -> Capability:
    """Convert a single catalog YAML entry to Capability dataclass.

    Internal helper. Raises CatalogError on missing/invalid fields.

    TODO(codex Slice a): implement per schema/agent-capability.schema.json.
    """
    raise NotImplementedError("Slice (a) — codex pickup; helper for load_catalog")
control-plane/platformctl/tests/agent_access/__init__.py
control-plane/platformctl/tests/agent_access/test_catalog.py
"""Contract test scaffold for capability catalog.

Per docs/specs/agent-access-plane-v0/03-tasks.md task a.5.

These tests assert the *interface*; Slice (a) implementation provides the
behavior. Codex MUST extend these with positive cases once load_catalog +
validate_catalog are implemented.
"""

from __future__ import annotations

import pytest

from platformctl.agent_access import (
    CatalogError,
    load_catalog,
    lookup_capability,
    validate_catalog,
)


pytestmark = pytest.mark.xfail(
    strict=False, reason="Slice (a) pre-impl scaffold — see 03-tasks.md task a.5"
)


def test_load_catalog_raises_until_implemented(tmp_path):
    """Until Slice (a) ships, load_catalog raises NotImplementedError."""
    catalog_path = tmp_path / "empty.yaml"
    catalog_path.write_text("schema_version: 1\ncapabilities: []\n")
    with pytest.raises(NotImplementedError):
        load_catalog(catalog_path)


def test_validate_catalog_raises_until_implemented(tmp_path):
    """Until Slice (a) ships, validate_catalog raises NotImplementedError."""
    catalog_path = tmp_path / "empty.yaml"
    catalog_path.write_text("schema_version: 1\ncapabilities: []\n")
    with pytest.raises(NotImplementedError):
        validate_catalog(catalog_path)


def test_lookup_capability_raises_until_implemented():
    """Until Slice (a) ships, lookup_capability raises NotImplementedError."""
    with pytest.raises(NotImplementedError):
        lookup_capability("anything", "codex", [])


# Post-Slice-(a) tests, currently xfail. Codex flips xfail off when implementing.


def test_valid_catalog_loads():
    """A valid catalog loads to list[Capability] without error."""
    raise NotImplementedError("Slice (a) — codex pickup")


def test_invalid_schema_version_rejected():
    """schema_version != 1 raises CatalogError."""
    raise NotImplementedError("Slice (a) — codex pickup")


def test_unknown_cousin_name_rejected():
    """cousin_allowed containing an unknown cousin name raises CatalogError."""
    raise NotImplementedError("Slice (a) — codex pickup")


def test_cousin_in_both_allowed_and_forbidden_rejected():
    """Per 00-constitution.md, a cousin in both lists is a semantic error."""
    raise NotImplementedError("Slice (a) — codex pickup")


def test_ttl_max_less_than_default_rejected():
    """ttl_max < ttl_default is invalid (cousin's default would exceed max)."""
    raise NotImplementedError("Slice (a) — codex pickup")


def test_lookup_forbidden_cousin_raises():
    """Per 01-specify.md FR-7, lookup for a forbidden cousin raises CatalogError."""
    raise NotImplementedError("Slice (a) — codex pickup")


def test_lookup_unallowed_cousin_raises():
    """Per 01-specify.md FR-7, lookup for a cousin not in allowed list raises."""
    raise NotImplementedError("Slice (a) — codex pickup")
docs/specs/agent-access-plane-v0/00-constitution.md
# Agent Access Plane v0 — Constitution

**Status:** draft (Spec Kit phase 1 of 6)
**Source:** issue #76 (codex-authored, parent for #56, #73, #74; ADR-0003 foundation)
**Author:** claude (this spec scaffolding)
**Implementer:** TBD (codex anticipated)

---

## Purpose

Agent Access Plane is the cross-cutting product for **session-scoped AI/operator credential delivery** on `pdurlej/platform` and connected runtimes (RS2000, VPS1000).

The plane is **policy + machinery for "agents get operator-approved capabilities during a bounded session; they do not receive standing raw credentials."**

This v0 establishes the platform-side foundation. Runtime-side OpenClaw implementation lives in `pdurlej/iskra-openclaw` (per ADR boundary).

---

## Principles (forbidden anti-patterns codified as positive rules)

### P1 — No standing raw credentials in agent context

Agents (codex, claude, glm, hermes, DeepSeek-v4-Pro, antigravity) MUST NOT receive long-lived API keys, SSH private keys, or database passwords as environment variables, shell history, argv, or sourced dotenv files visible to their session.

**Why:** any persistence of raw credentials in agent-visible state defeats the purpose of identity isolation (ADR-0010) and creates an exfiltration surface that scales linearly with cousin count.

### P2 — Infisical is the source of truth for AI/machine credentials

Every credential needed by an agent at runtime MUST resolve through Infisical (token auth or machine identity), with audit trail. No credential is "the truth" if it lives only in a shell script, an `.env` file, or a Forgejo repo secret.

**Exception:** runtime-local non-secret session state (`~/.platformctl-runtime/`) MAY hold ephemeral capability handles (e.g. `SSH_AUTH_SOCK`, time-bounded session IDs) explicitly allowed by policy.

### P3 — SSH uses ssh-agent sessions with TTL, not private keys

Agents requiring SSH MUST be issued a `SSH_AUTH_SOCK` pointing to a `ssh-agent` started with `-t <ttl>` and pre-loaded with the appropriate identity. Agents MUST NOT see the private key file path or content.

**Why:** ssh-agent provides operator-friendly TTL-bounded delegation. The agent gets capability; the agent never gets the key.

### P4 — Per-agent machine identities, not shared read-all identities

Each cousin (codex, claude, glm, ...) MUST have its own Infisical machine identity with scoped ACLs (project, environment, path glob). No cousin gets a shared `infisical-readonly` identity that has read-all access.

**Why:** shared identities defeat audit; a compromised cousin compromises all.

### P5 — Wrapper-injected secrets, not `infisical run -- bash`

Where a command needs secrets at execution time, the secrets MUST be injected by a wrapper script that:
- reads from Infisical via machine identity
- exports only the specific env vars needed by the wrapped command
- exits with the wrapped command's exit code
- writes audit log to `~/.platformctl-runtime/audit/` (non-secret)

Generic shell injection (`infisical run -- bash`) is forbidden because it gives the shell access to ALL secrets in scope, not just the ones the wrapped command needs.

### P6 — Runtime session state files are non-secret and private-mode

`~/.platformctl-runtime/` MAY hold:
- session IDs (random, time-bounded)
- capability handles (SSH_AUTH_SOCK, token file paths)
- audit logs (which capability was used when)
- non-secret bridge metadata (machine identity ID, ACL path, expiry timestamp)

`~/.platformctl-runtime/` MUST NOT hold:
- raw API keys, tokens, or passwords
- private SSH keys
- decrypted vault payloads

All files in this directory MUST be mode `0600` (operator/agent-only readable).

### P7 — Capability catalog declares the universe; nothing outside it can be injected

A central catalog (`policies/agent-capabilities.yaml`) MUST enumerate every capability a cousin can request. The runtime MUST refuse capability requests not in the catalog.

**Why:** capability sprawl is the slow-form of P1. If a cousin can request any random env var name, the discipline collapses.

### P8 — Operator approval is per-session, not per-PAT

A cousin starting a new session MUST request capabilities. Operator MAY approve session-scope (TTL ≤ session duration) or task-scope (TTL ≤ task duration). Standing per-PAT approval is forbidden for sensitive capabilities (any `class/security-sensitive` work).

For trivial work (read-only repo access, status checks), the cousin's identity PAT is sufficient — no per-session approval needed.

---

## Anti-patterns (forbidden in v0)

1. **`infisical run -- bash` for agent use** — see P5
2. **Sourced dotenv files visible to agent shell** — see P1
3. **Forgejo repo secrets carrying provider API keys** — Forgejo repo secrets are operator-trust-only; agents resolve through Infisical machine identity. See `docs/ci/runner-contract.md` for the deploy runner precedent.
4. **Symlinks from agent-writable paths into operator credentials** — sacred-path discipline applies
5. **In-process credential caching across sessions** — TTL ends, capability ends
6. **Logs / artifacts / evidence files containing credential values** — redaction is mandatory; see existing `apply.py` redaction patterns and extend
7. **Cousin-to-cousin credential sharing without operator-mediated handoff** — direct `cousin A passes token to cousin B` is forbidden
8. **Default-allow capability lists** — explicit allowlist only
9. **CI runner secrets baked into workflow YAML** — see `docs/ci/runner-contract.md` runner-local capability pattern
10. **"Sudo" capability tier for agents** — agents never have privileged escalation; operator-only

---

## Boundary with `pdurlej/iskra-openclaw`

| `pdurlej/platform` (this repo) | `pdurlej/iskra-openclaw` (runtime) |
|---|---|
| Policy: capability catalog, ACL templates, TTL defaults | Iskra-specific allowlists |
| Capability schema (machine-checkable) | SecretRef runtime behavior on OpenClaw |
| `platformctl` CLI + capability commands | Host-side wrappers on vps1000 |
| Tests + audit assertions | Iskra system tools that invoke wrappers |
| Documentation + ADRs | OpenClaw-specific operational runbooks |
| Trust anchor: Infisical | Trust anchor: imported from platform |

Cross-repo coordination: the platform schema is normative. OpenClaw consumes the schema, MUST NOT mutate it. Schema changes go through platform ADR + canary.

---

## North Star

> Operator can sleep knowing that no cousin agent, even if individually compromised, can persistently exfiltrate operator credentials, escalate to operator-tier capabilities, or chain another cousin's session into broader access than its own per-session approval.

If v0 doesn't deliver against this North Star, it's a v0 in name only. Falsifiable criteria in `01-specify.md` § Success.
docs/specs/agent-access-plane-v0/01-specify.md
# Agent Access Plane v0 — Specify

**Phase:** 2 of 6 (Spec Kit) | **Status:** draft
**Predecessor:** `00-constitution.md` | **Successor:** `02-plan.md`

---

## Functional Requirements (FRs)

### FR-1 — Capability catalog file

The platform MUST provide a `policies/agent-capabilities.yaml` file declaring every capability available to cousins. Each entry has:

```yaml
capabilities:
  - id: ssh-rs2000-platform-host-agent
    description: SSH access to rs2000 as platform-host-agent user for deploy operations
    cousin_allowed: [codex]
    cousin_forbidden: [glm, hermes]
    backing:
      type: ssh-agent
      key_source: infisical://prod/platform/rs2000/platform-host-agent-key
      ttl_default: 3600
      ttl_max: 14400
    audit:
      level: high
      required_evidence: command_log
```

### FR-2 — `platformctl access` subcommand

The platform MUST provide a `platformctl access` CLI subcommand with the following surface:

```
platformctl access list                            # list capabilities available to current cousin
platformctl access request <cap-id> [--ttl <sec>]  # request a capability; emits session token on approval
platformctl access show <session-id>               # show capability handle (path to ssh-agent socket, etc.)
platformctl access revoke <session-id>             # explicitly close a session before TTL
platformctl access audit [--cousin <name>] [--since <date>]  # query audit log
```

### FR-3 — Session state at `~/.platformctl-runtime/`

The platform MUST establish `~/.platformctl-runtime/` as the canonical agent runtime state root with:

```
~/.platformctl-runtime/
├── sessions/<session-id>.json         # active session manifest (non-secret)
├── agents/<agent-name>.sock           # ssh-agent sockets (mode 0600)
├── audit/<YYYY-MM-DD>.jsonl           # append-only audit log
└── policy/                            # cached capability catalog snapshot
```

### FR-4 — Infisical resolver integration

For capabilities backed by Infisical (most), the platform MUST resolve credentials through the existing Infisical machine identity pattern (per `control-plane/platformctl/apply.py` `_forgejo_token_from_infisical` precedent). Cousin MUST NOT see Infisical token, only the resolved capability handle.

### FR-5 — Operator approval flow

For capabilities with `audit.level: high`, the platform MUST request operator approval (out-of-band: Forgejo issue comment, Signal notification, or operator terminal) before issuing the capability. Approval encoded in audit log with operator identity reference.

### FR-6 — Cousin-PAT-driven identity for low-tier requests

For capabilities with `audit.level: low` (read-only repo access, status checks), the cousin's existing Forgejo PAT is sufficient. No new session needed. Audit log still receives entry.

### FR-7 — Per-cousin capability ACL enforcement

The runtime MUST refuse capability requests when `cousin not in cap.cousin_allowed` OR `cousin in cap.cousin_forbidden`. Refusal logged. Operator notified on cross-cousin attempts (potential identity confusion).

### FR-8 — TTL enforcement

The runtime MUST forcefully revoke capabilities at TTL expiry. ssh-agent processes terminated. Session manifest moved to `audit/expired/`. Cousin's next call returns explicit `expired` error code.

### FR-9 — Audit log immutability

Audit log entries are append-only JSONL. The platform MUST NOT support `platformctl access audit --delete` or similar. Operator can rotate audit log files (size-bounded), but not edit them.

### FR-10 — Hostname allowlist

For capabilities backed by SSH, the catalog declares allowed target hostnames (Tailnet IPs/names). The runtime MUST refuse SSH commands targeting hosts outside the allowlist. Cross-references issue #198.

### FR-11 — Wrapper-injected secrets for command capabilities

For capabilities of type `wrapped-command` (e.g., `docker login`, `infisical secrets read`), a wrapper script is generated per-session in `~/.platformctl-runtime/wrappers/<session-id>/<command>`. Wrapper:
- reads secret from Infisical at execution time
- exports only the env vars the wrapped command needs
- redacts the env vars from `env`-like introspection
- exits with wrapped command's exit code

---

## Non-Functional Requirements (NFRs)

### NFR-1 — No raw credentials in shell history or argv

Verified by adversarial tests that scan `history`, `ps`, audit log for known secret patterns.

### NFR-2 — Session creation latency < 2 seconds (p95)

Operator approval flow is async (out-of-band); machine-only flows must complete fast.

### NFR-3 — Audit log size bounded

Daily rotation, 90-day retention. Operator can opt-in to longer retention via config.

### NFR-4 — Schema versioned

Capability catalog YAML carries `schema_version: 1`. v0.1 → v0.2 schema changes go through ADR.

### NFR-5 — Cross-host compatibility (RS2000, VPS1000)

Same `platformctl access` surface works on operator's Mac, RS2000, VPS1000. Single binary path.

### NFR-6 — Operator override

Operator can issue any capability to any cousin via `platformctl access grant --cousin <name> --cap <id> --ttl <sec> --reason "<note>"`. Operator override entries flagged in audit log.

---

## Open Questions

### Q1 — Where does ssh-agent live across cousin sessions?

Option A: per-session ssh-agent (clean, short-lived, but cousin restarts mean re-request).
Option B: per-cousin long-lived ssh-agent with TTL-bounded identities (slot reuse, faster).

**Default direction:** Option A for v0 (simpler, audit-clean). Migrate to B if Option A measurably slow.

### Q2 — Capability catalog merge semantics

If `policies/agent-capabilities.yaml` is amended in a PR, does the runtime auto-pick-up after PR merge, or require explicit `platformctl access reload`?

**Default direction:** auto-pick-up on next session creation; existing sessions keep their capability handle until TTL expiry.

### Q3 — Cross-repo capability declarations

OpenClaw runtime needs Iskra-specific capabilities (vault read, Signal send). Should these live in `pdurlej/iskra-openclaw/policies/iskra-capabilities.yaml` and be imported, or in platform catalog with `runtime: openclaw` tag?

**Default direction:** in-platform catalog with `runtime: openclaw` tag. Single source of truth for capability schema; OpenClaw runtime validates against same schema.

### Q4 — Break-glass capability

Operator break-glass (full-access capability, time-bounded, high-audit) — is this in catalog or out-of-band?

**Default direction:** in catalog with `cousin_allowed: [operator-only]`, `audit.level: critical`, `ttl_max: 900` (15 min). Operator-only means no cousin can request; only operator can `grant`.

### Q5 — Audit log access for cousins

Can a cousin read its own audit log? Can it read other cousins' audit logs?

**Default direction:** own log read-allowed; cross-cousin read forbidden. Operator can read all.

---

## Success Criteria (falsifiable)

v0 is successful when:

1. **No cousin commit contains a credential.** Pre-commit hook + canary check enforces. Verified via gitleaks scan over 30-day rolling window.
2. **No cousin session has non-Infisical-sourced credentials in its env, argv, or shell history.** Verified via adversarial test suite (NFR-1).
3. **`platformctl access list` works for every cousin on every supported host.** Verified via cross-host smoke test.
4. **Operator approval flow latency for high-audit capability < 5 minutes (manual approval window).** Verified by test campaign with operator-on-duty.
5. **Capability ACL refusal works for every `cousin_forbidden` pairing.** Verified via adversarial test (cousin A attempts capability assigned to cousin B).
6. **TTL enforcement empirically observed.** Test: capability issued with TTL=60s, used at t=0, t=30, t=120. Third usage MUST fail with explicit `expired` error.
7. **Audit log entries are immutable in production.** Verified via integrity hash check on rotated files.
8. **ADR-0003 (Agent Access Plane) is updated to reflect v0 actuals.** Spec drift between ADR and code triggers test failure.

If any of 1-7 fails post-implementation, the v0 declaration is rolled back and the failing surface is treated as a P0 follow-up.

---

## Out of Scope (v0)

- YubiKey-backed approval (issue #132 — separate PR)
- Cloud-agent-fabric integration (Iskra remote orchestration)
- Hardware-encrypted vault tier (issue #178, #180 — Tier 2)
- Multi-operator support (single-operator platform per North Star)
- UI for operator-side approval (CLI + Signal notification is v0; UI deferred)
- Cross-platform identity federation (out of scope; identity is per-cousin per-platform)
docs/specs/agent-access-plane-v0/02-plan.md
# Agent Access Plane v0 — Plan

**Phase:** 3 of 6 | **Status:** draft
**Predecessor:** `01-specify.md` | **Successor:** `03-tasks.md`

---

## 4 Slices (a / b / c / d)

### Slice (a) — Catalog schema + validator

**Scope:** define the capability catalog format, write a JSON Schema, write `platformctl access validate` subcommand that loads + validates catalog.

**Output:**
- `schema/agent-capability.schema.json` (JSON Schema)
- `policies/agent-capabilities.yaml` (catalog file, seeded with 3-5 example capabilities)
- `control-plane/platformctl/agent_access/catalog.py` (loader + validator)
- Unit tests for valid/invalid catalog inputs

**Acceptance:**
- `platformctl access validate` exits 0 on a valid catalog
- Exits nonzero with structured error on missing required fields, invalid TTL bounds, unknown cousin name, etc.
- ≥ 95% line coverage on catalog.py

**Effort:** S (~300 LOC)

### Slice (b) — Session machinery + audit log

**Scope:** implement session creation, capability handle issuance, audit log write, TTL expiry enforcement.

**Output:**
- `control-plane/platformctl/agent_access/session.py` (session lifecycle)
- `control-plane/platformctl/agent_access/audit.py` (audit log writer)
- `control-plane/platformctl/agent_access/expiry.py` (TTL daemon or in-process expiry check)
- CLI: `platformctl access request`, `platformctl access show`, `platformctl access revoke`, `platformctl access audit`
- `~/.platformctl-runtime/` directory bootstrap

**Acceptance:**
- Session created + manifest written to disk + audit log entry appended
- TTL expiry observably revokes session (adversarial test)
- `platformctl access audit` queries readable
- All session files mode `0600`

**Effort:** M (~600 LOC)

### Slice (c) — Infisical resolver + ssh-agent backing

**Scope:** for capabilities of type `ssh-agent`, integrate with Infisical to fetch the key, start ssh-agent with TTL, return `SSH_AUTH_SOCK` handle to cousin.

**Output:**
- `control-plane/platformctl/agent_access/backings/infisical.py` (resolver)
- `control-plane/platformctl/agent_access/backings/ssh_agent.py` (ssh-agent lifecycle)
- Wrapper for the existing `_forgejo_token_from_infisical` precedent (DRY)
- Integration test using local Infisical instance (rs2000 docker)

**Acceptance:**
- Cousin can request ssh-agent capability; receives `SSH_AUTH_SOCK` path
- ssh-agent process terminates at TTL
- Cousin's `ssh` command works against allowlisted host
- Cousin's `ssh` against non-allowlisted host fails closed

**Effort:** M (~500 LOC)

### Slice (d) — Operator approval flow + wrapper-command capabilities

**Scope:** for `audit.level: high` capabilities, implement async operator approval. For `wrapped-command` capabilities, generate per-session wrapper scripts.

**Output:**
- `control-plane/platformctl/agent_access/approval.py` (async approval)
- `control-plane/platformctl/agent_access/wrapper.py` (wrapper script generator)
- Signal notification integration (via existing `home-platform-signal-cli` if available)
- Forgejo issue-comment notification (fallback)

**Acceptance:**
- High-audit capability request blocks until operator approves out-of-band
- Approval timeout (configurable, default 5 min) → request fails
- Wrapper script generated for a `docker login`-like capability; secret never visible in `env`/`ps`
- Audit log captures who approved + when

**Effort:** M (~600 LOC)

---

## Milestones

| Milestone | Date | Definition of done |
|---|---|---|
| **M1: Catalog ships** | Week 1 | Slice (a) merged; first 5 capabilities catalogued; `platformctl access validate` green |
| **M2: Sessions ship** | Week 2 | Slice (b) merged; one cousin can request + use + revoke a "trivial" capability end-to-end |
| **M3: SSH backing ships** | Week 3 | Slice (c) merged; codex can SSH to rs2000 via session token, audit log clean |
| **M4: Approval flow ships** | Week 4 | Slice (d) merged; operator-on-duty can approve via Signal; high-audit caps work |
| **M5: ADR-0003 reconciled** | Week 4-5 | ADR updated to reflect actuals; success criteria 1-7 from spec validated; documentation cross-references complete |

**Total v0 ETA:** ~4 weeks of cousin-execution time (codex primary, claude review, glm reviewer-of-last-resort).

---

## Risks (with mitigations)

| Risk | Impact | Mitigation |
|---|---|---|
| Infisical access from non-rs2000 host (operator Mac) requires VPN/Tailnet → resolver fails | M | Resolver gracefully degrades to "request capability from rs2000-broker"; cousin runs from rs2000 by default |
| Per-session ssh-agent has process-table pollution at scale | L | Slice (b) acceptance includes ps-clean assertion; agent process named for traceability |
| Operator approval via Signal is operator-attention-cost if frequent | M | High-audit threshold tuned conservatively; operator can grant batch approval ("approve next 3 codex requests") |
| Audit log size growth | L | Daily rotation + size-bounded retention enforced |
| Capability catalog drift between platform and OpenClaw runtimes | M | OpenClaw imports schema; OpenClaw CI validates against platform schema; drift = test failure |
| TTL expiry mid-command (e.g., long-running docker build) | M | Wrappers re-resolve at command boundary, NOT mid-process; long-running commands need explicit refresh capability |

---

## Rollback strategy

Slice (a): catalog-only. Remove file; revert PR.

Slice (b): session machinery. Disable subcommands via feature flag; sessions auto-expire; no production impact.

Slice (c): SSH backing. Revert PR; cousin reverts to existing PAT-based SSH (current state).

Slice (d): approval flow + wrappers. Revert PR; high-audit capabilities revert to "operator manually injects" (current state). Wrappers cease functioning at expiry; no persistent state harm.

**No slice creates persistent state that survives revert.** This is by design — v0 must be removable without restore.

---

## Cross-repo dependencies

- `pdurlej/iskra-openclaw` MUST import the capability schema (read-only) once Slice (a) lands.
- `pdurlej/iskra-openclaw#43` (SecretRef architecture) is reframed by this v0; coordinate close with operator before declaring superseded.

---

## Composability proof — future Slice (e+)

The Slice (a)-(d) v0 is designed to be composable with:

- **#132 YubiKey-backed operator consent** — YubiKey approval slots into Slice (d) `approval.py` as an additional approval backend. No core architectural change required.
- **#73 codex→OpenClaw SSH** — first concrete use case. Adds one capability to catalog. Validates end-to-end loop.
- **#56 per-agent Forgejo/MCP identity split** — Forgejo PAT delivery slots into Slice (c) as a new backing type. Inherits ACL + audit.
- **Cloud-agent-fabric (Iskra remote orchestration)** — adds `remote-broker` capability type; broker becomes an authorized cousin with its own catalog ACL.

If any future slice cannot compose cleanly with v0, the v0 design has a gap — flag in `04-implement-notes.md`.
<!-- codex-source-branch-salvage:v1 source=claude/feat-agent-access-plane part=1/2 --> ## Codex source-branch salvage appendix (1/2) **Source branch:** `claude/feat-agent-access-plane` **Attached to:** ollama/dziadek-agent-capability-catalog / PR #566 Preserves the full Agent Access Plane Spec Kit and stub code omitted from the narrowed capability-catalog PR. This is archival/reference material copied before deleting stale source branches. It is not an approval to merge the old design/code as-is. <details> <summary><code>control-plane/platformctl/agent_access/catalog.py</code></summary> <pre><code>&quot;&quot;&quot;Capability catalog loader + validator — Slice (a) scaffold. Per docs/specs/agent-access-plane-v0/03-tasks.md tasks a.3, a.4. This is pre-impl scaffold. Codex picks up Slice (a) and fills in: - yaml.safe_load with structured error reporting - JSONSchema validation against schema/agent-capability.schema.json - lookup_capability with cousin ACL check &quot;&quot;&quot; from __future__ import annotations from pathlib import Path from typing import Any from ._types import ( AuditLevel, BackingType, Capability, CapabilityBacking, ) class CatalogError(Exception): &quot;&quot;&quot;Raised on catalog load/validation failures.&quot;&quot;&quot; def load_catalog(path: str | Path) -&gt; list[Capability]: &quot;&quot;&quot;Load and parse the capability catalog YAML. Args: path: filesystem path to catalog YAML (typically policies/agent-capabilities.yaml). Returns: List of Capability dataclasses, one per catalog entry. Raises: CatalogError: on parse failure, schema violation, or semantic error (e.g., cousin name in both allowed AND forbidden). TODO(codex Slice a): implement with yaml.safe_load + JSONSchema validation + semantic checks per 03-tasks.md task a.3. &quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup; see 03-tasks.md task a.3&quot;) def validate_catalog(path: str | Path) -&gt; list[str]: &quot;&quot;&quot;Validate the capability catalog without loading it into runtime memory. Args: path: filesystem path to catalog YAML. Returns: List of validation error messages. Empty list means catalog is valid. TODO(codex Slice a): implement per 03-tasks.md task a.4. CLI exit code: 0 for valid (empty list), 1 for invalid. &quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup; see 03-tasks.md task a.4&quot;) def lookup_capability( cap_id: str, cousin: str, catalog: list[Capability] ) -&gt; Capability: &quot;&quot;&quot;Find capability by ID, verifying cousin is in allowed list and not forbidden. Args: cap_id: capability ID (matches Capability.id). cousin: cousin name (claude/codex/glm/...). catalog: loaded catalog. Returns: Capability dataclass. Raises: CatalogError: cap_id not found, cousin in cousin_forbidden, or cousin not in cousin_allowed. TODO(codex Slice a): implement per 03-tasks.md task a.3. Per Constitution P7 and FR-7, refusal must be loud + audit-logged. &quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup; see 03-tasks.md task a.3&quot;) def _parse_capability_dict(raw: dict[str, Any]) -&gt; Capability: &quot;&quot;&quot;Convert a single catalog YAML entry to Capability dataclass. Internal helper. Raises CatalogError on missing/invalid fields. TODO(codex Slice a): implement per schema/agent-capability.schema.json. &quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup; helper for load_catalog&quot;) </code></pre> </details> <details> <summary><code>control-plane/platformctl/tests/agent_access/__init__.py</code></summary> <pre><code></code></pre> </details> <details> <summary><code>control-plane/platformctl/tests/agent_access/test_catalog.py</code></summary> <pre><code>&quot;&quot;&quot;Contract test scaffold for capability catalog. Per docs/specs/agent-access-plane-v0/03-tasks.md task a.5. These tests assert the *interface*; Slice (a) implementation provides the behavior. Codex MUST extend these with positive cases once load_catalog + validate_catalog are implemented. &quot;&quot;&quot; from __future__ import annotations import pytest from platformctl.agent_access import ( CatalogError, load_catalog, lookup_capability, validate_catalog, ) pytestmark = pytest.mark.xfail( strict=False, reason=&quot;Slice (a) pre-impl scaffold — see 03-tasks.md task a.5&quot; ) def test_load_catalog_raises_until_implemented(tmp_path): &quot;&quot;&quot;Until Slice (a) ships, load_catalog raises NotImplementedError.&quot;&quot;&quot; catalog_path = tmp_path / &quot;empty.yaml&quot; catalog_path.write_text(&quot;schema_version: 1\ncapabilities: []\n&quot;) with pytest.raises(NotImplementedError): load_catalog(catalog_path) def test_validate_catalog_raises_until_implemented(tmp_path): &quot;&quot;&quot;Until Slice (a) ships, validate_catalog raises NotImplementedError.&quot;&quot;&quot; catalog_path = tmp_path / &quot;empty.yaml&quot; catalog_path.write_text(&quot;schema_version: 1\ncapabilities: []\n&quot;) with pytest.raises(NotImplementedError): validate_catalog(catalog_path) def test_lookup_capability_raises_until_implemented(): &quot;&quot;&quot;Until Slice (a) ships, lookup_capability raises NotImplementedError.&quot;&quot;&quot; with pytest.raises(NotImplementedError): lookup_capability(&quot;anything&quot;, &quot;codex&quot;, []) # Post-Slice-(a) tests, currently xfail. Codex flips xfail off when implementing. def test_valid_catalog_loads(): &quot;&quot;&quot;A valid catalog loads to list[Capability] without error.&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) def test_invalid_schema_version_rejected(): &quot;&quot;&quot;schema_version != 1 raises CatalogError.&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) def test_unknown_cousin_name_rejected(): &quot;&quot;&quot;cousin_allowed containing an unknown cousin name raises CatalogError.&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) def test_cousin_in_both_allowed_and_forbidden_rejected(): &quot;&quot;&quot;Per 00-constitution.md, a cousin in both lists is a semantic error.&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) def test_ttl_max_less_than_default_rejected(): &quot;&quot;&quot;ttl_max &lt; ttl_default is invalid (cousin&#x27;s default would exceed max).&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) def test_lookup_forbidden_cousin_raises(): &quot;&quot;&quot;Per 01-specify.md FR-7, lookup for a forbidden cousin raises CatalogError.&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) def test_lookup_unallowed_cousin_raises(): &quot;&quot;&quot;Per 01-specify.md FR-7, lookup for a cousin not in allowed list raises.&quot;&quot;&quot; raise NotImplementedError(&quot;Slice (a) — codex pickup&quot;) </code></pre> </details> <details> <summary><code>docs/specs/agent-access-plane-v0/00-constitution.md</code></summary> <pre><code># Agent Access Plane v0 — Constitution **Status:** draft (Spec Kit phase 1 of 6) **Source:** issue #76 (codex-authored, parent for #56, #73, #74; ADR-0003 foundation) **Author:** claude (this spec scaffolding) **Implementer:** TBD (codex anticipated) --- ## Purpose Agent Access Plane is the cross-cutting product for **session-scoped AI/operator credential delivery** on `pdurlej/platform` and connected runtimes (RS2000, VPS1000). The plane is **policy + machinery for &quot;agents get operator-approved capabilities during a bounded session; they do not receive standing raw credentials.&quot;** This v0 establishes the platform-side foundation. Runtime-side OpenClaw implementation lives in `pdurlej/iskra-openclaw` (per ADR boundary). --- ## Principles (forbidden anti-patterns codified as positive rules) ### P1 — No standing raw credentials in agent context Agents (codex, claude, glm, hermes, DeepSeek-v4-Pro, antigravity) MUST NOT receive long-lived API keys, SSH private keys, or database passwords as environment variables, shell history, argv, or sourced dotenv files visible to their session. **Why:** any persistence of raw credentials in agent-visible state defeats the purpose of identity isolation (ADR-0010) and creates an exfiltration surface that scales linearly with cousin count. ### P2 — Infisical is the source of truth for AI/machine credentials Every credential needed by an agent at runtime MUST resolve through Infisical (token auth or machine identity), with audit trail. No credential is &quot;the truth&quot; if it lives only in a shell script, an `.env` file, or a Forgejo repo secret. **Exception:** runtime-local non-secret session state (`~/.platformctl-runtime/`) MAY hold ephemeral capability handles (e.g. `SSH_AUTH_SOCK`, time-bounded session IDs) explicitly allowed by policy. ### P3 — SSH uses ssh-agent sessions with TTL, not private keys Agents requiring SSH MUST be issued a `SSH_AUTH_SOCK` pointing to a `ssh-agent` started with `-t &lt;ttl&gt;` and pre-loaded with the appropriate identity. Agents MUST NOT see the private key file path or content. **Why:** ssh-agent provides operator-friendly TTL-bounded delegation. The agent gets capability; the agent never gets the key. ### P4 — Per-agent machine identities, not shared read-all identities Each cousin (codex, claude, glm, ...) MUST have its own Infisical machine identity with scoped ACLs (project, environment, path glob). No cousin gets a shared `infisical-readonly` identity that has read-all access. **Why:** shared identities defeat audit; a compromised cousin compromises all. ### P5 — Wrapper-injected secrets, not `infisical run -- bash` Where a command needs secrets at execution time, the secrets MUST be injected by a wrapper script that: - reads from Infisical via machine identity - exports only the specific env vars needed by the wrapped command - exits with the wrapped command&#x27;s exit code - writes audit log to `~/.platformctl-runtime/audit/` (non-secret) Generic shell injection (`infisical run -- bash`) is forbidden because it gives the shell access to ALL secrets in scope, not just the ones the wrapped command needs. ### P6 — Runtime session state files are non-secret and private-mode `~/.platformctl-runtime/` MAY hold: - session IDs (random, time-bounded) - capability handles (SSH_AUTH_SOCK, token file paths) - audit logs (which capability was used when) - non-secret bridge metadata (machine identity ID, ACL path, expiry timestamp) `~/.platformctl-runtime/` MUST NOT hold: - raw API keys, tokens, or passwords - private SSH keys - decrypted vault payloads All files in this directory MUST be mode `0600` (operator/agent-only readable). ### P7 — Capability catalog declares the universe; nothing outside it can be injected A central catalog (`policies/agent-capabilities.yaml`) MUST enumerate every capability a cousin can request. The runtime MUST refuse capability requests not in the catalog. **Why:** capability sprawl is the slow-form of P1. If a cousin can request any random env var name, the discipline collapses. ### P8 — Operator approval is per-session, not per-PAT A cousin starting a new session MUST request capabilities. Operator MAY approve session-scope (TTL ≤ session duration) or task-scope (TTL ≤ task duration). Standing per-PAT approval is forbidden for sensitive capabilities (any `class/security-sensitive` work). For trivial work (read-only repo access, status checks), the cousin&#x27;s identity PAT is sufficient — no per-session approval needed. --- ## Anti-patterns (forbidden in v0) 1. **`infisical run -- bash` for agent use** — see P5 2. **Sourced dotenv files visible to agent shell** — see P1 3. **Forgejo repo secrets carrying provider API keys** — Forgejo repo secrets are operator-trust-only; agents resolve through Infisical machine identity. See `docs/ci/runner-contract.md` for the deploy runner precedent. 4. **Symlinks from agent-writable paths into operator credentials** — sacred-path discipline applies 5. **In-process credential caching across sessions** — TTL ends, capability ends 6. **Logs / artifacts / evidence files containing credential values** — redaction is mandatory; see existing `apply.py` redaction patterns and extend 7. **Cousin-to-cousin credential sharing without operator-mediated handoff** — direct `cousin A passes token to cousin B` is forbidden 8. **Default-allow capability lists** — explicit allowlist only 9. **CI runner secrets baked into workflow YAML** — see `docs/ci/runner-contract.md` runner-local capability pattern 10. **&quot;Sudo&quot; capability tier for agents** — agents never have privileged escalation; operator-only --- ## Boundary with `pdurlej/iskra-openclaw` | `pdurlej/platform` (this repo) | `pdurlej/iskra-openclaw` (runtime) | |---|---| | Policy: capability catalog, ACL templates, TTL defaults | Iskra-specific allowlists | | Capability schema (machine-checkable) | SecretRef runtime behavior on OpenClaw | | `platformctl` CLI + capability commands | Host-side wrappers on vps1000 | | Tests + audit assertions | Iskra system tools that invoke wrappers | | Documentation + ADRs | OpenClaw-specific operational runbooks | | Trust anchor: Infisical | Trust anchor: imported from platform | Cross-repo coordination: the platform schema is normative. OpenClaw consumes the schema, MUST NOT mutate it. Schema changes go through platform ADR + canary. --- ## North Star &gt; Operator can sleep knowing that no cousin agent, even if individually compromised, can persistently exfiltrate operator credentials, escalate to operator-tier capabilities, or chain another cousin&#x27;s session into broader access than its own per-session approval. If v0 doesn&#x27;t deliver against this North Star, it&#x27;s a v0 in name only. Falsifiable criteria in `01-specify.md` § Success. </code></pre> </details> <details> <summary><code>docs/specs/agent-access-plane-v0/01-specify.md</code></summary> <pre><code># Agent Access Plane v0 — Specify **Phase:** 2 of 6 (Spec Kit) | **Status:** draft **Predecessor:** `00-constitution.md` | **Successor:** `02-plan.md` --- ## Functional Requirements (FRs) ### FR-1 — Capability catalog file The platform MUST provide a `policies/agent-capabilities.yaml` file declaring every capability available to cousins. Each entry has: ```yaml capabilities: - id: ssh-rs2000-platform-host-agent description: SSH access to rs2000 as platform-host-agent user for deploy operations cousin_allowed: [codex] cousin_forbidden: [glm, hermes] backing: type: ssh-agent key_source: infisical://prod/platform/rs2000/platform-host-agent-key ttl_default: 3600 ttl_max: 14400 audit: level: high required_evidence: command_log ``` ### FR-2 — `platformctl access` subcommand The platform MUST provide a `platformctl access` CLI subcommand with the following surface: ``` platformctl access list # list capabilities available to current cousin platformctl access request &lt;cap-id&gt; [--ttl &lt;sec&gt;] # request a capability; emits session token on approval platformctl access show &lt;session-id&gt; # show capability handle (path to ssh-agent socket, etc.) platformctl access revoke &lt;session-id&gt; # explicitly close a session before TTL platformctl access audit [--cousin &lt;name&gt;] [--since &lt;date&gt;] # query audit log ``` ### FR-3 — Session state at `~/.platformctl-runtime/` The platform MUST establish `~/.platformctl-runtime/` as the canonical agent runtime state root with: ``` ~/.platformctl-runtime/ ├── sessions/&lt;session-id&gt;.json # active session manifest (non-secret) ├── agents/&lt;agent-name&gt;.sock # ssh-agent sockets (mode 0600) ├── audit/&lt;YYYY-MM-DD&gt;.jsonl # append-only audit log └── policy/ # cached capability catalog snapshot ``` ### FR-4 — Infisical resolver integration For capabilities backed by Infisical (most), the platform MUST resolve credentials through the existing Infisical machine identity pattern (per `control-plane/platformctl/apply.py` `_forgejo_token_from_infisical` precedent). Cousin MUST NOT see Infisical token, only the resolved capability handle. ### FR-5 — Operator approval flow For capabilities with `audit.level: high`, the platform MUST request operator approval (out-of-band: Forgejo issue comment, Signal notification, or operator terminal) before issuing the capability. Approval encoded in audit log with operator identity reference. ### FR-6 — Cousin-PAT-driven identity for low-tier requests For capabilities with `audit.level: low` (read-only repo access, status checks), the cousin&#x27;s existing Forgejo PAT is sufficient. No new session needed. Audit log still receives entry. ### FR-7 — Per-cousin capability ACL enforcement The runtime MUST refuse capability requests when `cousin not in cap.cousin_allowed` OR `cousin in cap.cousin_forbidden`. Refusal logged. Operator notified on cross-cousin attempts (potential identity confusion). ### FR-8 — TTL enforcement The runtime MUST forcefully revoke capabilities at TTL expiry. ssh-agent processes terminated. Session manifest moved to `audit/expired/`. Cousin&#x27;s next call returns explicit `expired` error code. ### FR-9 — Audit log immutability Audit log entries are append-only JSONL. The platform MUST NOT support `platformctl access audit --delete` or similar. Operator can rotate audit log files (size-bounded), but not edit them. ### FR-10 — Hostname allowlist For capabilities backed by SSH, the catalog declares allowed target hostnames (Tailnet IPs/names). The runtime MUST refuse SSH commands targeting hosts outside the allowlist. Cross-references issue #198. ### FR-11 — Wrapper-injected secrets for command capabilities For capabilities of type `wrapped-command` (e.g., `docker login`, `infisical secrets read`), a wrapper script is generated per-session in `~/.platformctl-runtime/wrappers/&lt;session-id&gt;/&lt;command&gt;`. Wrapper: - reads secret from Infisical at execution time - exports only the env vars the wrapped command needs - redacts the env vars from `env`-like introspection - exits with wrapped command&#x27;s exit code --- ## Non-Functional Requirements (NFRs) ### NFR-1 — No raw credentials in shell history or argv Verified by adversarial tests that scan `history`, `ps`, audit log for known secret patterns. ### NFR-2 — Session creation latency &lt; 2 seconds (p95) Operator approval flow is async (out-of-band); machine-only flows must complete fast. ### NFR-3 — Audit log size bounded Daily rotation, 90-day retention. Operator can opt-in to longer retention via config. ### NFR-4 — Schema versioned Capability catalog YAML carries `schema_version: 1`. v0.1 → v0.2 schema changes go through ADR. ### NFR-5 — Cross-host compatibility (RS2000, VPS1000) Same `platformctl access` surface works on operator&#x27;s Mac, RS2000, VPS1000. Single binary path. ### NFR-6 — Operator override Operator can issue any capability to any cousin via `platformctl access grant --cousin &lt;name&gt; --cap &lt;id&gt; --ttl &lt;sec&gt; --reason &quot;&lt;note&gt;&quot;`. Operator override entries flagged in audit log. --- ## Open Questions ### Q1 — Where does ssh-agent live across cousin sessions? Option A: per-session ssh-agent (clean, short-lived, but cousin restarts mean re-request). Option B: per-cousin long-lived ssh-agent with TTL-bounded identities (slot reuse, faster). **Default direction:** Option A for v0 (simpler, audit-clean). Migrate to B if Option A measurably slow. ### Q2 — Capability catalog merge semantics If `policies/agent-capabilities.yaml` is amended in a PR, does the runtime auto-pick-up after PR merge, or require explicit `platformctl access reload`? **Default direction:** auto-pick-up on next session creation; existing sessions keep their capability handle until TTL expiry. ### Q3 — Cross-repo capability declarations OpenClaw runtime needs Iskra-specific capabilities (vault read, Signal send). Should these live in `pdurlej/iskra-openclaw/policies/iskra-capabilities.yaml` and be imported, or in platform catalog with `runtime: openclaw` tag? **Default direction:** in-platform catalog with `runtime: openclaw` tag. Single source of truth for capability schema; OpenClaw runtime validates against same schema. ### Q4 — Break-glass capability Operator break-glass (full-access capability, time-bounded, high-audit) — is this in catalog or out-of-band? **Default direction:** in catalog with `cousin_allowed: [operator-only]`, `audit.level: critical`, `ttl_max: 900` (15 min). Operator-only means no cousin can request; only operator can `grant`. ### Q5 — Audit log access for cousins Can a cousin read its own audit log? Can it read other cousins&#x27; audit logs? **Default direction:** own log read-allowed; cross-cousin read forbidden. Operator can read all. --- ## Success Criteria (falsifiable) v0 is successful when: 1. **No cousin commit contains a credential.** Pre-commit hook + canary check enforces. Verified via gitleaks scan over 30-day rolling window. 2. **No cousin session has non-Infisical-sourced credentials in its env, argv, or shell history.** Verified via adversarial test suite (NFR-1). 3. **`platformctl access list` works for every cousin on every supported host.** Verified via cross-host smoke test. 4. **Operator approval flow latency for high-audit capability &lt; 5 minutes (manual approval window).** Verified by test campaign with operator-on-duty. 5. **Capability ACL refusal works for every `cousin_forbidden` pairing.** Verified via adversarial test (cousin A attempts capability assigned to cousin B). 6. **TTL enforcement empirically observed.** Test: capability issued with TTL=60s, used at t=0, t=30, t=120. Third usage MUST fail with explicit `expired` error. 7. **Audit log entries are immutable in production.** Verified via integrity hash check on rotated files. 8. **ADR-0003 (Agent Access Plane) is updated to reflect v0 actuals.** Spec drift between ADR and code triggers test failure. If any of 1-7 fails post-implementation, the v0 declaration is rolled back and the failing surface is treated as a P0 follow-up. --- ## Out of Scope (v0) - YubiKey-backed approval (issue #132 — separate PR) - Cloud-agent-fabric integration (Iskra remote orchestration) - Hardware-encrypted vault tier (issue #178, #180 — Tier 2) - Multi-operator support (single-operator platform per North Star) - UI for operator-side approval (CLI + Signal notification is v0; UI deferred) - Cross-platform identity federation (out of scope; identity is per-cousin per-platform) </code></pre> </details> <details> <summary><code>docs/specs/agent-access-plane-v0/02-plan.md</code></summary> <pre><code># Agent Access Plane v0 — Plan **Phase:** 3 of 6 | **Status:** draft **Predecessor:** `01-specify.md` | **Successor:** `03-tasks.md` --- ## 4 Slices (a / b / c / d) ### Slice (a) — Catalog schema + validator **Scope:** define the capability catalog format, write a JSON Schema, write `platformctl access validate` subcommand that loads + validates catalog. **Output:** - `schema/agent-capability.schema.json` (JSON Schema) - `policies/agent-capabilities.yaml` (catalog file, seeded with 3-5 example capabilities) - `control-plane/platformctl/agent_access/catalog.py` (loader + validator) - Unit tests for valid/invalid catalog inputs **Acceptance:** - `platformctl access validate` exits 0 on a valid catalog - Exits nonzero with structured error on missing required fields, invalid TTL bounds, unknown cousin name, etc. - ≥ 95% line coverage on catalog.py **Effort:** S (~300 LOC) ### Slice (b) — Session machinery + audit log **Scope:** implement session creation, capability handle issuance, audit log write, TTL expiry enforcement. **Output:** - `control-plane/platformctl/agent_access/session.py` (session lifecycle) - `control-plane/platformctl/agent_access/audit.py` (audit log writer) - `control-plane/platformctl/agent_access/expiry.py` (TTL daemon or in-process expiry check) - CLI: `platformctl access request`, `platformctl access show`, `platformctl access revoke`, `platformctl access audit` - `~/.platformctl-runtime/` directory bootstrap **Acceptance:** - Session created + manifest written to disk + audit log entry appended - TTL expiry observably revokes session (adversarial test) - `platformctl access audit` queries readable - All session files mode `0600` **Effort:** M (~600 LOC) ### Slice (c) — Infisical resolver + ssh-agent backing **Scope:** for capabilities of type `ssh-agent`, integrate with Infisical to fetch the key, start ssh-agent with TTL, return `SSH_AUTH_SOCK` handle to cousin. **Output:** - `control-plane/platformctl/agent_access/backings/infisical.py` (resolver) - `control-plane/platformctl/agent_access/backings/ssh_agent.py` (ssh-agent lifecycle) - Wrapper for the existing `_forgejo_token_from_infisical` precedent (DRY) - Integration test using local Infisical instance (rs2000 docker) **Acceptance:** - Cousin can request ssh-agent capability; receives `SSH_AUTH_SOCK` path - ssh-agent process terminates at TTL - Cousin&#x27;s `ssh` command works against allowlisted host - Cousin&#x27;s `ssh` against non-allowlisted host fails closed **Effort:** M (~500 LOC) ### Slice (d) — Operator approval flow + wrapper-command capabilities **Scope:** for `audit.level: high` capabilities, implement async operator approval. For `wrapped-command` capabilities, generate per-session wrapper scripts. **Output:** - `control-plane/platformctl/agent_access/approval.py` (async approval) - `control-plane/platformctl/agent_access/wrapper.py` (wrapper script generator) - Signal notification integration (via existing `home-platform-signal-cli` if available) - Forgejo issue-comment notification (fallback) **Acceptance:** - High-audit capability request blocks until operator approves out-of-band - Approval timeout (configurable, default 5 min) → request fails - Wrapper script generated for a `docker login`-like capability; secret never visible in `env`/`ps` - Audit log captures who approved + when **Effort:** M (~600 LOC) --- ## Milestones | Milestone | Date | Definition of done | |---|---|---| | **M1: Catalog ships** | Week 1 | Slice (a) merged; first 5 capabilities catalogued; `platformctl access validate` green | | **M2: Sessions ship** | Week 2 | Slice (b) merged; one cousin can request + use + revoke a &quot;trivial&quot; capability end-to-end | | **M3: SSH backing ships** | Week 3 | Slice (c) merged; codex can SSH to rs2000 via session token, audit log clean | | **M4: Approval flow ships** | Week 4 | Slice (d) merged; operator-on-duty can approve via Signal; high-audit caps work | | **M5: ADR-0003 reconciled** | Week 4-5 | ADR updated to reflect actuals; success criteria 1-7 from spec validated; documentation cross-references complete | **Total v0 ETA:** ~4 weeks of cousin-execution time (codex primary, claude review, glm reviewer-of-last-resort). --- ## Risks (with mitigations) | Risk | Impact | Mitigation | |---|---|---| | Infisical access from non-rs2000 host (operator Mac) requires VPN/Tailnet → resolver fails | M | Resolver gracefully degrades to &quot;request capability from rs2000-broker&quot;; cousin runs from rs2000 by default | | Per-session ssh-agent has process-table pollution at scale | L | Slice (b) acceptance includes ps-clean assertion; agent process named for traceability | | Operator approval via Signal is operator-attention-cost if frequent | M | High-audit threshold tuned conservatively; operator can grant batch approval (&quot;approve next 3 codex requests&quot;) | | Audit log size growth | L | Daily rotation + size-bounded retention enforced | | Capability catalog drift between platform and OpenClaw runtimes | M | OpenClaw imports schema; OpenClaw CI validates against platform schema; drift = test failure | | TTL expiry mid-command (e.g., long-running docker build) | M | Wrappers re-resolve at command boundary, NOT mid-process; long-running commands need explicit refresh capability | --- ## Rollback strategy Slice (a): catalog-only. Remove file; revert PR. Slice (b): session machinery. Disable subcommands via feature flag; sessions auto-expire; no production impact. Slice (c): SSH backing. Revert PR; cousin reverts to existing PAT-based SSH (current state). Slice (d): approval flow + wrappers. Revert PR; high-audit capabilities revert to &quot;operator manually injects&quot; (current state). Wrappers cease functioning at expiry; no persistent state harm. **No slice creates persistent state that survives revert.** This is by design — v0 must be removable without restore. --- ## Cross-repo dependencies - `pdurlej/iskra-openclaw` MUST import the capability schema (read-only) once Slice (a) lands. - `pdurlej/iskra-openclaw#43` (SecretRef architecture) is reframed by this v0; coordinate close with operator before declaring superseded. --- ## Composability proof — future Slice (e+) The Slice (a)-(d) v0 is designed to be composable with: - **#132 YubiKey-backed operator consent** — YubiKey approval slots into Slice (d) `approval.py` as an additional approval backend. No core architectural change required. - **#73 codex→OpenClaw SSH** — first concrete use case. Adds one capability to catalog. Validates end-to-end loop. - **#56 per-agent Forgejo/MCP identity split** — Forgejo PAT delivery slots into Slice (c) as a new backing type. Inherits ACL + audit. - **Cloud-agent-fabric (Iskra remote orchestration)** — adds `remote-broker` capability type; broker becomes an authorized cousin with its own catalog ACL. If any future slice cannot compose cleanly with v0, the v0 design has a gap — flag in `04-implement-notes.md`. </code></pre> </details>
Collaborator

Codex source-branch salvage appendix (2/2)

Source branch: claude/feat-agent-access-plane
Attached to: ollama/dziadek-agent-capability-catalog / PR #566

Preserves the full Agent Access Plane Spec Kit and stub code omitted from the narrowed capability-catalog PR.

This is archival/reference material copied before deleting stale source branches. It is not an approval to merge the old design/code as-is.

docs/specs/agent-access-plane-v0/03-tasks.md
# Agent Access Plane v0 — Tasks

**Phase:** 4 of 6 | **Status:** draft
**Predecessor:** `02-plan.md` | **Successor:** `04-implement-notes.md`

Atomic task list. Each task targets ≤ 200 LOC delta and a single concern. Tasks are ordered within slice but may parallel across slices once dependencies satisfied.

---

## Slice (a) — Catalog schema + validator (M1, ~300 LOC)

- **a.1** Write `schema/agent-capability.schema.json` covering: `id`, `description`, `cousin_allowed`, `cousin_forbidden`, `backing` (oneOf: ssh-agent/wrapped-command/infisical-token), `audit.level` (low/medium/high/critical), `audit.required_evidence`, `ttl_default`, `ttl_max`, `allowed_hosts` (optional, for ssh-agent type). Include `schema_version: 1`. (S, ~100 LOC)

- **a.2** Seed `policies/agent-capabilities.yaml` with 5 example capabilities: read-only repo access, ssh-rs2000-deploy, ssh-vps1000-iskra-readonly, infisical-secrets-read-scoped, forgejo-pat-derived-write. Each example must validate. (S, ~80 LOC)

- **a.3** Implement `control-plane/platformctl/agent_access/catalog.py` (`load_catalog()`, `validate_catalog()`, `lookup_capability(cap_id, cousin)` returning Capability dataclass). (S, ~120 LOC)

- **a.4** CLI: `platformctl access validate [path]`. Returns 0 on valid, 1 with structured error report on invalid. (S, ~50 LOC)

- **a.5** Unit tests `test_catalog.py`: valid case, missing required field, invalid TTL bounds (max < default), unknown cousin name, forbidden + allowed overlap (rejected), unknown audit.level. (S, ~150 LOC)

- **a.6** Update `AGENTS.md` § Conventions with capability-catalog overview + pointer to schema. (S, ~30 LOC)

---

## Slice (b) — Session machinery + audit log (M2, ~600 LOC)

- **b.1** Define `Session`, `AuditEntry`, `SessionStatus` dataclasses in `agent_access/_types.py`. (S, ~100 LOC)

- **b.2** Implement `agent_access/session.py` with `create_session()`, `revoke_session()`, `get_session()`, `cleanup_expired()`. State persisted to `~/.platformctl-runtime/sessions/<id>.json` mode 0600. (M, ~200 LOC)

- **b.3** Implement `agent_access/audit.py` with append-only JSONL writer. Entries: session_id, cousin, capability_id, action (request/grant/use/revoke/expire), timestamp, operator_approver (if any), evidence_ref (if any). Daily rotation by filename. (S, ~150 LOC)

- **b.4** Implement `agent_access/expiry.py` — lazy expiry check on every operation; optional `platformctl access expire-sweep` cron-friendly subcommand. (S, ~100 LOC)

- **b.5** CLI: `platformctl access request`, `show`, `revoke`, `audit`. (S, ~150 LOC)

- **b.6** Integration test `test_session_lifecycle.py`: request → show → use (simulated) → audit log assertions → revoke → re-show returns expired. (M, ~200 LOC)

- **b.7** Bootstrap `~/.platformctl-runtime/` on first use; refuse to operate if directory exists with wrong perms. (S, ~50 LOC)

---

## Slice (c) — Infisical resolver + ssh-agent backing (M3, ~500 LOC)

- **c.1** Refactor `apply.py:_forgejo_token_from_infisical` into shared `agent_access/backings/infisical.py:resolve()` with `path`, `key`, `env` parameters. apply.py becomes a caller. (S, ~150 LOC, DRY refactor)

- **c.2** Implement `agent_access/backings/ssh_agent.py`: `start_agent(ttl)`, `add_key_from_infisical(path, key_id)`, `socket_path()`, `stop_agent()`. Manages `ssh-agent -t <ttl>` subprocess, captures `SSH_AUTH_SOCK`, persists to session manifest. (M, ~250 LOC)

- **c.3** Wire `backings/ssh_agent.py` into session creation: when `capability.backing.type == ssh-agent`, create + add key + return socket path to cousin. (S, ~50 LOC)

- **c.4** Hostname allowlist enforcement: SSH capability declares `allowed_hosts`; wrapper or session refuses if cousin attempts non-allowlisted host. (S, ~100 LOC)

- **c.5** Integration test `test_ssh_agent_backing.py`: request ssh-agent capability → socket exists → key loadable → ssh to allowlisted host succeeds (mocked) → ssh to non-allowlisted host fails. TTL expiry kills agent. (M, ~150 LOC)

---

## Slice (d) — Operator approval flow + wrapped-command capabilities (M4, ~600 LOC)

- **d.1** Implement `agent_access/approval.py`: `request_approval(session_id, capability, reason)`, polling for operator response, configurable timeout. (M, ~200 LOC)

- **d.2** Approval backends: `signal_cli_notification`, `forgejo_issue_comment`, `terminal_prompt` (operator-attended). Pluggable. (M, ~200 LOC)

- **d.3** Implement `agent_access/wrapper.py`: generate per-session wrapper script for `wrapped-command` capabilities. Wrapper resolves secret via Infisical at exec time, exports specific env vars, redacts from `env` (where possible), exits with wrapped command's exit code. (M, ~200 LOC)

- **d.4** CLI: `platformctl access grant` (operator-side, out-of-band override). Audit log flags operator override entries. (S, ~80 LOC)

- **d.5** Integration test `test_approval_flow.py`: high-audit capability request → approval timeout → second request → approval granted → audit log verifies operator identity. (M, ~150 LOC)

- **d.6** Adversarial test `test_no_credential_leaks.py`: scan session env, audit log, shell history, `ps` output for known secret patterns. Must find zero leaks. (M, ~150 LOC)

---

## Cross-slice tasks

- **x.1** Update `decisions/0003-agent-access-plane.md` ADR with v0 implementation pointers + reconciliation with v0 actuals. (S, ~100 LOC)

- **x.2** Update `docs/ci/runner-contract.md` to point to capability catalog for runner-local secret resolution. (S, ~50 LOC)

- **x.3** Add `state/STATUS_NOW.md` line for "agent-access plane v0 status". (S, ~10 LOC, Trivial PR)

- **x.4** New issue: track YubiKey approval backend (composability with #132). (S, comment-only)

- **x.5** New issue: track cloud-agent-fabric capability type (composability with Iskra remote orchestration). (S, comment-only)

---

## Estimated total LOC

| Slice | LOC |
|---|---|
| (a) | ~300 |
| (b) | ~600 |
| (c) | ~500 |
| (d) | ~600 |
| Cross-slice | ~200 |
| **Total v0** | **~2200 LOC** |

Plus ~600 LOC tests (already counted in slice estimates).

---

## Codex execution notes

When executing this v0:

- Each slice is one PR. Per ADR-0007 risk-tier: Slice (a) Lite; Slice (b)/(c)/(d) Full (security-sensitive, schema-change, sacred-path-adjacent).
- Slice (a) MUST land + soak (≥48h) before Slice (b) starts. No stacked PRs (ADR-0017).
- Each PR includes Canary Context Pack per `AGENTS.md` § Canary Context Pack.
- Adversarial tests (d.6, c.5 non-allowlisted host case) MUST run on a real codex/claude/glm cousin identity, NOT a synthetic one. Otherwise the discipline is theater.
- If a slice exceeds ~700 LOC delta, split into 2 PRs. Better atomic.
docs/specs/agent-access-plane-v0/04-implement-notes.md
# Agent Access Plane v0 — Implementation notes

**Phase:** 5 of 6 | **Status:** draft (pre-impl scaffolding)
**Predecessor:** `03-tasks.md` | **Successor:** `README.md`

This file is **growable** — implementers (codex primarily) append decisions, cross-refs, and unanticipated tradeoffs as they execute. Pre-impl scaffolding seeds it.

---

## Pre-impl decisions log

### D1 — Catalog file format: YAML, not JSON

**Decision:** `policies/agent-capabilities.yaml`, not `.json`.
**Rationale:** YAML is operator-friendly for comment annotation; capabilities ARE policy and benefit from inline rationale. JSON Schema validates the YAML at load time (schema/agent-capability.schema.json).

### D2 — Session ID: ULID, not UUID

**Decision:** Session IDs are 26-char ULIDs (time-ordered + monotonic).
**Rationale:** filesystem listings of `~/.platformctl-runtime/sessions/` sort by creation time naturally. Audit log entries chronological without separate timestamp index.

### D3 — Audit log: JSONL, not SQLite

**Decision:** `~/.platformctl-runtime/audit/<YYYY-MM-DD>.jsonl` append-only.
**Rationale:** append-only file is easier to reason about than DB transactions. Cross-tool grep-able. Rotation is filesystem-level. Cross-references the `iskra-openclaw` telemetry JSONL boundary precedent (ADR-0004).

### D4 — TTL enforcement: lazy + sweep, not real-time

**Decision:** TTL expiry checked on every `platformctl access` call. Optional `platformctl access expire-sweep` for cron-friendly proactive cleanup.
**Rationale:** real-time TTL would require a daemon or scheduler. Lazy + sweep is simpler and meets correctness — capability handle becomes invalid at TTL; cousin can't use it; audit reflects expiry.
**Tradeoff:** ssh-agent process may continue running past TTL until next sweep. Mitigated by ssh-agent's own `-t <ttl>` flag which kills the agent process at expiry.

### D5 — Operator approval: out-of-band, not in-CLI

**Decision:** for high-audit capabilities, the cousin's request blocks; operator approves via Signal message, Forgejo issue comment, or terminal command. Cousin never sees operator credentials.
**Rationale:** in-CLI approval would require operator to be on-host. Out-of-band lets operator approve from phone, Mac, or remote terminal. Matches operator's mobile-first reality.
**Open:** approval forgery vector if Signal/Forgejo identity is spoofed. Mitigation: approval messages carry session_id + cousin name; operator must echo them back. Tighter: HMAC-signed approval challenge.

### D6 — `audit.level: critical` is operator-only

**Decision:** no cousin can request `critical`-level capability. Operator can `grant` directly.
**Rationale:** critical = sacred-path-adjacent, break-glass-style. Putting it in the cousin request path tempts agents to escalate. Operator-only by design.

### D7 — Composability over completeness

**Decision:** v0 ships 4 slices. YubiKey, cloud-fabric, multi-operator are explicit follow-up issues, NOT v0.
**Rationale:** see operator's North Star + ADR-0018. Ship a v0 that works for the 3 main slices (codex SSH to rs2000, codex Infisical read, hermes wrapped-command). Defer everything else.

### D8 — Single Mac binary

**Decision:** `platformctl` binary is the same on operator Mac, RS2000, VPS1000.
**Rationale:** no fragmentation of CLI surface. Existing `control-plane/platformctl/` is one Python package. Tests + CI ensure cross-host parity.

### D9 — ssh-agent per session, NOT per cousin

**Decision:** per-session ssh-agent (Slice c, FR-3, Q1 option A).
**Rationale:** clean lifecycle, audit-clear, no state reuse across sessions.
**Migration path:** if measurably slow (NFR-2: < 2s p95), revisit Option B (per-cousin agent with TTL-bounded identities).

### D10 — Wrapper-script approach for `wrapped-command` capabilities

**Decision:** generate per-session wrapper script in `~/.platformctl-runtime/wrappers/<session-id>/<command-name>`. Wrapper:
- shebang `#!/usr/bin/env bash` (POSIX, portable)
- reads secret via Infisical CLI at exec time
- exports specific env vars (declared in capability spec)
- `exec` wrapped command
**Rationale:** Python isn't always on cousin host. Bash is. Wrapper is reviewable. Secret never persisted to disk.

### D11 — Cousin identity for low-tier requests = existing Forgejo PAT

**Decision:** for `audit.level: low` capabilities (read-only repo status, list issues, etc.), no new session needed. Cousin uses existing Forgejo PAT from `security find-internet-password -s git.pdurlej.com -a <cousin> -w`.
**Rationale:** AGENTS.md already establishes this PAT pattern. Don't reinvent. Add audit log entry on use; that's the discipline addition.

### D12 — `~/.platformctl-runtime/` is NOT sacred path

**Decision:** `~/.platformctl-runtime/` MAY be deleted by operator at any time without data loss. Sessions revoke, agents stop, audit log preserved separately if needed.
**Rationale:** sacred-path discipline applies to data-of-record (postgres volumes, vault snapshots, env files). Runtime state is by definition ephemeral. Allow operator to "reset" cleanly.

---

## Cross-reference matrix

| Spec section | Source/related |
|---|---|
| Slice (a) catalog schema | `schema/module.schema.json` precedent (existing) |
| Slice (b) audit log | ADR-0004 Iskra tool telemetry JSONL boundary |
| Slice (c) Infisical resolver | `control-plane/platformctl/apply.py:_forgejo_token_from_infisical` |
| Slice (c) ssh-agent | `runbooks/forgejo-actions-runner.md` (existing SSH discipline) |
| Slice (d) approval | new — no existing precedent |
| Constitution P5 wrapper rule | `docs/ci/runner-contract.md` runner-local capability pattern |
| Hostname allowlist (FR-10) | issue #198 HOST-ALLOWLIST-01 |
| Redaction (NFR-1, anti-pattern #6) | issues #199, #200 REDACT-COMMAND-01, REDACT-EXTEND-01 |
| Cousin identity (ADR-0010) | every section |
| Single status file (ADR-0006) | x.3 STATUS_NOW update task |
| Risk-proportional canary (ADR-0007) | Codex execution notes in 03-tasks.md |
| No stacked PRs (ADR-0017) | Codex execution notes — one slice = one PR |
| Fix root causes, not workarounds (ADR-0018) | Acceptance criteria — no "accept workaround" outcomes |

---

## Composability proof

This v0 must compose cleanly with future work:

- **YubiKey approval (issue #132)** — slots into `approval.py` as a new backend in d.2. No core change.
- **Codex→OpenClaw SSH (issue #73)** — first concrete capability; validates Slice (c). One catalog entry.
- **Per-agent Forgejo/MCP identity split (issue #56)** — slots into `backings/` as a Forgejo PAT backing type. Reuses session machinery.
- **Cloud-agent-fabric** — new backing type `remote-broker`; broker is itself an authorized cousin. No schema change.
- **Operator-side break-glass via YubiKey** — `audit.level: critical` + YubiKey approval. Composable.

If any future capability cannot express in this schema, the schema has a gap. Track as ADR.

---

## Known unknowns

1. **Infisical access from operator Mac** — does the Mac have direct Tailnet route to local Infisical, or does it need to go via rs2000 broker? Test in Slice (a) integration.
2. **Signal-cli identity for approval messages** — does the existing `home-platform-signal-cli` have a programmatic send API, or just CLI? Determines `approval.py` implementation.
3. **Forgejo notification permissions** — can an issue comment from `claude` cousin trigger a Signal notification for operator? Test in Slice (d).
4. **Lifetime of wrapper scripts** — at session expiry, wrapper scripts are removed. But what if cousin already started a wrapped command and the command is long-running? Wrapper-internal `exec` should survive ssh-agent expiry. Verify.
5. **Cross-cousin audit log queries** — `platformctl access audit --cousin codex` from claude's session — is this allowed? D11 says own log read OK, cross forbidden. Operator only.

---

## Implementation order suggestion (codex pickup hint)

If you (codex / claude / glm executing this v0) are picking up the work:

1. **Read this spec in order** (00 → 01 → 02 → 03 → 04 → README). Don't skip 00 — it has the non-negotiables.
2. **Confirm scope with operator before starting Slice (a).** Capability schema is the foundation; getting it wrong cascades.
3. **Implement Slice (a) end-to-end + tests + canary review** before touching (b). Each slice is a PR; no stacks.
4. **Use existing precedents** (apply.py Infisical resolver, audit log JSONL pattern, ssh-agent runbook discipline). DRY > novelty.
5. **Adversarial tests are MANDATORY** for Slice (d). If they pass with synthetic secrets, they prove nothing. Real cousin identity, real PAT, real ssh-agent. See d.6.
6. **Update this doc** as you implement. Decisions D13+, lessons learned, surprises. This is the durable record for the next cousin who reads it cold.
docs/specs/agent-access-plane-v0/README.md
# Agent Access Plane v0 — Spec Kit

**Phase:** 6 of 6 (index) | **Status:** draft — for review, NOT for merge
**Source:** issue #76 (codex-authored)
**Author:** claude (pre-impl scaffolding for codex pickup)
**Pattern:** GitHub Spec Kit hybrid (constitution / specify / plan / tasks / impl-notes)
**Precedent:** `pdurlej/iskra-openclaw` PR #276, #278, #279, #281 (same pattern, different repo)

---

## What's in this directory

| File | Phase | Purpose |
|---|---|---|
| [00-constitution.md](00-constitution.md) | 1 | Non-negotiable principles + forbidden anti-patterns |
| [01-specify.md](01-specify.md) | 2 | Functional + non-functional requirements + open questions + falsifiable success criteria |
| [02-plan.md](02-plan.md) | 3 | 4 slices (a/b/c/d), milestones, risks, rollback strategy |
| [03-tasks.md](03-tasks.md) | 4 | Atomic task list, LOC estimates, codex execution notes |
| [04-implement-notes.md](04-implement-notes.md) | 5 | Decisions log, cross-references, composability proof, codex pickup hints |
| [README.md](README.md) | 6 | This file (index) |

---

## How to read this

**If you are operator (piotr):** read this README + `00-constitution.md` § Principles + `01-specify.md` § Success Criteria. ~15 min total. Decide: accept/reject/amend the v0 scope. Operator-only signal.

**If you are codex (executor):** read all 6 files in order. ~45 min. Then start Slice (a). Per `03-tasks.md` § Codex execution notes.

**If you are claude (reviewer):** read all 6 files + cross-check against ADR-0003, ADR-0005, ADR-0010, ADR-0018. Flag drift.

**If you are DeepSeek-v4-Pro (deep reviewer):** read all 6 files + assess composability claims in `04-implement-notes.md`. Look for blindspots the in-loop cousins missed.

**If you are glm (cheap-pass reviewer):** read `00-constitution.md` + `02-plan.md` § Risks. Smell-check.

---

## TL;DR

Agents (codex, claude, glm, ...) currently access platform credentials via per-cousin Forgejo PATs and ad-hoc shell environments. This works but creates exfiltration surface that scales with cousin count.

**Agent Access Plane v0** introduces:
- A **capability catalog** (`policies/agent-capabilities.yaml`) declaring what each cousin can request
- A **session machinery** (`platformctl access`) that issues TTL-bounded, audit-logged capability handles
- **Infisical-backed credential resolution** at the moment of use, never persisted to agent shell
- **Operator approval** for high-audit capabilities, out-of-band (Signal / Forgejo / terminal)
- **Wrapper scripts** for command capabilities, never leaking secrets to argv or env introspection

**Cousin gets capability handle. Cousin never gets the credential.**

v0 ships in 4 slices over ~4 weeks of cousin-execution time. No slice creates persistent state that survives revert.

---

## Status (this PR)

| Surface | Status |
|---|---|
| Spec Kit (6 markdowns) | ✅ scaffold ready |
| Code skeleton (`control-plane/platformctl/agent_access/`) | ✅ minimal scaffold (types, catalog, CLI stub) |
| Schema (`schema/agent-capability.schema.json`) | ✅ draft |
| Seed catalog (`policies/agent-capabilities.yaml`) | ✅ 5 example capabilities |
| Tests | ⚠️ contract test scaffold only |
| Slice (a) implementation | ❌ TODO (codex) |
| Slice (b) implementation | ❌ TODO (codex) |
| Slice (c) implementation | ❌ TODO (codex) |
| Slice (d) implementation | ❌ TODO (codex) |

**This PR is WIP draft. Not for merge.** Operator review of scope → operator approves → codex picks up Slice (a) in a new PR.

---

## Related issues

- **#76** (parent) — Agent Access Plane product framing
- **#73** — codex→OpenClaw SSH (first concrete capability use case)
- **#56** — per-agent Forgejo/MCP identity split (composes with v0 via Forgejo PAT backing type)
- **#72** — Forgejo Actions/canary Infisical integration (resolver pattern)
- **#74** — repo-boundary decision (resolved by ADR-0003 + this spec)
- **#132** — YubiKey approval (composes with v0 via approval backend)
- **#198** — host allowlist (composes with v0 FR-10)
- **#199, #200** — redaction (NFR-1 + Constitution anti-pattern #6)
- **`pdurlej/iskra-openclaw#43`** — superseded by this v0; cross-repo coordination needed

---

## ADRs touched

- **ADR-0003** (Agent Access Plane) — updated to reflect v0 actuals as part of x.1
- **ADR-0005** (coordination lanes) — referenced; not modified
- **ADR-0007** (risk-proportional canary) — applied to Slice (a)-(d) PR tier classification
- **ADR-0010** (cousin role taxonomy) — referenced; not modified
- **ADR-0017** (no stacked PRs) — applied to Slice execution order
- **ADR-0018** (fix root causes, not workarounds) — applied to acceptance criteria
<!-- codex-source-branch-salvage:v1 source=claude/feat-agent-access-plane part=2/2 --> ## Codex source-branch salvage appendix (2/2) **Source branch:** `claude/feat-agent-access-plane` **Attached to:** ollama/dziadek-agent-capability-catalog / PR #566 Preserves the full Agent Access Plane Spec Kit and stub code omitted from the narrowed capability-catalog PR. This is archival/reference material copied before deleting stale source branches. It is not an approval to merge the old design/code as-is. <details> <summary><code>docs/specs/agent-access-plane-v0/03-tasks.md</code></summary> <pre><code># Agent Access Plane v0 — Tasks **Phase:** 4 of 6 | **Status:** draft **Predecessor:** `02-plan.md` | **Successor:** `04-implement-notes.md` Atomic task list. Each task targets ≤ 200 LOC delta and a single concern. Tasks are ordered within slice but may parallel across slices once dependencies satisfied. --- ## Slice (a) — Catalog schema + validator (M1, ~300 LOC) - **a.1** Write `schema/agent-capability.schema.json` covering: `id`, `description`, `cousin_allowed`, `cousin_forbidden`, `backing` (oneOf: ssh-agent/wrapped-command/infisical-token), `audit.level` (low/medium/high/critical), `audit.required_evidence`, `ttl_default`, `ttl_max`, `allowed_hosts` (optional, for ssh-agent type). Include `schema_version: 1`. (S, ~100 LOC) - **a.2** Seed `policies/agent-capabilities.yaml` with 5 example capabilities: read-only repo access, ssh-rs2000-deploy, ssh-vps1000-iskra-readonly, infisical-secrets-read-scoped, forgejo-pat-derived-write. Each example must validate. (S, ~80 LOC) - **a.3** Implement `control-plane/platformctl/agent_access/catalog.py` (`load_catalog()`, `validate_catalog()`, `lookup_capability(cap_id, cousin)` returning Capability dataclass). (S, ~120 LOC) - **a.4** CLI: `platformctl access validate [path]`. Returns 0 on valid, 1 with structured error report on invalid. (S, ~50 LOC) - **a.5** Unit tests `test_catalog.py`: valid case, missing required field, invalid TTL bounds (max &lt; default), unknown cousin name, forbidden + allowed overlap (rejected), unknown audit.level. (S, ~150 LOC) - **a.6** Update `AGENTS.md` § Conventions with capability-catalog overview + pointer to schema. (S, ~30 LOC) --- ## Slice (b) — Session machinery + audit log (M2, ~600 LOC) - **b.1** Define `Session`, `AuditEntry`, `SessionStatus` dataclasses in `agent_access/_types.py`. (S, ~100 LOC) - **b.2** Implement `agent_access/session.py` with `create_session()`, `revoke_session()`, `get_session()`, `cleanup_expired()`. State persisted to `~/.platformctl-runtime/sessions/&lt;id&gt;.json` mode 0600. (M, ~200 LOC) - **b.3** Implement `agent_access/audit.py` with append-only JSONL writer. Entries: session_id, cousin, capability_id, action (request/grant/use/revoke/expire), timestamp, operator_approver (if any), evidence_ref (if any). Daily rotation by filename. (S, ~150 LOC) - **b.4** Implement `agent_access/expiry.py` — lazy expiry check on every operation; optional `platformctl access expire-sweep` cron-friendly subcommand. (S, ~100 LOC) - **b.5** CLI: `platformctl access request`, `show`, `revoke`, `audit`. (S, ~150 LOC) - **b.6** Integration test `test_session_lifecycle.py`: request → show → use (simulated) → audit log assertions → revoke → re-show returns expired. (M, ~200 LOC) - **b.7** Bootstrap `~/.platformctl-runtime/` on first use; refuse to operate if directory exists with wrong perms. (S, ~50 LOC) --- ## Slice (c) — Infisical resolver + ssh-agent backing (M3, ~500 LOC) - **c.1** Refactor `apply.py:_forgejo_token_from_infisical` into shared `agent_access/backings/infisical.py:resolve()` with `path`, `key`, `env` parameters. apply.py becomes a caller. (S, ~150 LOC, DRY refactor) - **c.2** Implement `agent_access/backings/ssh_agent.py`: `start_agent(ttl)`, `add_key_from_infisical(path, key_id)`, `socket_path()`, `stop_agent()`. Manages `ssh-agent -t &lt;ttl&gt;` subprocess, captures `SSH_AUTH_SOCK`, persists to session manifest. (M, ~250 LOC) - **c.3** Wire `backings/ssh_agent.py` into session creation: when `capability.backing.type == ssh-agent`, create + add key + return socket path to cousin. (S, ~50 LOC) - **c.4** Hostname allowlist enforcement: SSH capability declares `allowed_hosts`; wrapper or session refuses if cousin attempts non-allowlisted host. (S, ~100 LOC) - **c.5** Integration test `test_ssh_agent_backing.py`: request ssh-agent capability → socket exists → key loadable → ssh to allowlisted host succeeds (mocked) → ssh to non-allowlisted host fails. TTL expiry kills agent. (M, ~150 LOC) --- ## Slice (d) — Operator approval flow + wrapped-command capabilities (M4, ~600 LOC) - **d.1** Implement `agent_access/approval.py`: `request_approval(session_id, capability, reason)`, polling for operator response, configurable timeout. (M, ~200 LOC) - **d.2** Approval backends: `signal_cli_notification`, `forgejo_issue_comment`, `terminal_prompt` (operator-attended). Pluggable. (M, ~200 LOC) - **d.3** Implement `agent_access/wrapper.py`: generate per-session wrapper script for `wrapped-command` capabilities. Wrapper resolves secret via Infisical at exec time, exports specific env vars, redacts from `env` (where possible), exits with wrapped command&#x27;s exit code. (M, ~200 LOC) - **d.4** CLI: `platformctl access grant` (operator-side, out-of-band override). Audit log flags operator override entries. (S, ~80 LOC) - **d.5** Integration test `test_approval_flow.py`: high-audit capability request → approval timeout → second request → approval granted → audit log verifies operator identity. (M, ~150 LOC) - **d.6** Adversarial test `test_no_credential_leaks.py`: scan session env, audit log, shell history, `ps` output for known secret patterns. Must find zero leaks. (M, ~150 LOC) --- ## Cross-slice tasks - **x.1** Update `decisions/0003-agent-access-plane.md` ADR with v0 implementation pointers + reconciliation with v0 actuals. (S, ~100 LOC) - **x.2** Update `docs/ci/runner-contract.md` to point to capability catalog for runner-local secret resolution. (S, ~50 LOC) - **x.3** Add `state/STATUS_NOW.md` line for &quot;agent-access plane v0 status&quot;. (S, ~10 LOC, Trivial PR) - **x.4** New issue: track YubiKey approval backend (composability with #132). (S, comment-only) - **x.5** New issue: track cloud-agent-fabric capability type (composability with Iskra remote orchestration). (S, comment-only) --- ## Estimated total LOC | Slice | LOC | |---|---| | (a) | ~300 | | (b) | ~600 | | (c) | ~500 | | (d) | ~600 | | Cross-slice | ~200 | | **Total v0** | **~2200 LOC** | Plus ~600 LOC tests (already counted in slice estimates). --- ## Codex execution notes When executing this v0: - Each slice is one PR. Per ADR-0007 risk-tier: Slice (a) Lite; Slice (b)/(c)/(d) Full (security-sensitive, schema-change, sacred-path-adjacent). - Slice (a) MUST land + soak (≥48h) before Slice (b) starts. No stacked PRs (ADR-0017). - Each PR includes Canary Context Pack per `AGENTS.md` § Canary Context Pack. - Adversarial tests (d.6, c.5 non-allowlisted host case) MUST run on a real codex/claude/glm cousin identity, NOT a synthetic one. Otherwise the discipline is theater. - If a slice exceeds ~700 LOC delta, split into 2 PRs. Better atomic. </code></pre> </details> <details> <summary><code>docs/specs/agent-access-plane-v0/04-implement-notes.md</code></summary> <pre><code># Agent Access Plane v0 — Implementation notes **Phase:** 5 of 6 | **Status:** draft (pre-impl scaffolding) **Predecessor:** `03-tasks.md` | **Successor:** `README.md` This file is **growable** — implementers (codex primarily) append decisions, cross-refs, and unanticipated tradeoffs as they execute. Pre-impl scaffolding seeds it. --- ## Pre-impl decisions log ### D1 — Catalog file format: YAML, not JSON **Decision:** `policies/agent-capabilities.yaml`, not `.json`. **Rationale:** YAML is operator-friendly for comment annotation; capabilities ARE policy and benefit from inline rationale. JSON Schema validates the YAML at load time (schema/agent-capability.schema.json). ### D2 — Session ID: ULID, not UUID **Decision:** Session IDs are 26-char ULIDs (time-ordered + monotonic). **Rationale:** filesystem listings of `~/.platformctl-runtime/sessions/` sort by creation time naturally. Audit log entries chronological without separate timestamp index. ### D3 — Audit log: JSONL, not SQLite **Decision:** `~/.platformctl-runtime/audit/&lt;YYYY-MM-DD&gt;.jsonl` append-only. **Rationale:** append-only file is easier to reason about than DB transactions. Cross-tool grep-able. Rotation is filesystem-level. Cross-references the `iskra-openclaw` telemetry JSONL boundary precedent (ADR-0004). ### D4 — TTL enforcement: lazy + sweep, not real-time **Decision:** TTL expiry checked on every `platformctl access` call. Optional `platformctl access expire-sweep` for cron-friendly proactive cleanup. **Rationale:** real-time TTL would require a daemon or scheduler. Lazy + sweep is simpler and meets correctness — capability handle becomes invalid at TTL; cousin can&#x27;t use it; audit reflects expiry. **Tradeoff:** ssh-agent process may continue running past TTL until next sweep. Mitigated by ssh-agent&#x27;s own `-t &lt;ttl&gt;` flag which kills the agent process at expiry. ### D5 — Operator approval: out-of-band, not in-CLI **Decision:** for high-audit capabilities, the cousin&#x27;s request blocks; operator approves via Signal message, Forgejo issue comment, or terminal command. Cousin never sees operator credentials. **Rationale:** in-CLI approval would require operator to be on-host. Out-of-band lets operator approve from phone, Mac, or remote terminal. Matches operator&#x27;s mobile-first reality. **Open:** approval forgery vector if Signal/Forgejo identity is spoofed. Mitigation: approval messages carry session_id + cousin name; operator must echo them back. Tighter: HMAC-signed approval challenge. ### D6 — `audit.level: critical` is operator-only **Decision:** no cousin can request `critical`-level capability. Operator can `grant` directly. **Rationale:** critical = sacred-path-adjacent, break-glass-style. Putting it in the cousin request path tempts agents to escalate. Operator-only by design. ### D7 — Composability over completeness **Decision:** v0 ships 4 slices. YubiKey, cloud-fabric, multi-operator are explicit follow-up issues, NOT v0. **Rationale:** see operator&#x27;s North Star + ADR-0018. Ship a v0 that works for the 3 main slices (codex SSH to rs2000, codex Infisical read, hermes wrapped-command). Defer everything else. ### D8 — Single Mac binary **Decision:** `platformctl` binary is the same on operator Mac, RS2000, VPS1000. **Rationale:** no fragmentation of CLI surface. Existing `control-plane/platformctl/` is one Python package. Tests + CI ensure cross-host parity. ### D9 — ssh-agent per session, NOT per cousin **Decision:** per-session ssh-agent (Slice c, FR-3, Q1 option A). **Rationale:** clean lifecycle, audit-clear, no state reuse across sessions. **Migration path:** if measurably slow (NFR-2: &lt; 2s p95), revisit Option B (per-cousin agent with TTL-bounded identities). ### D10 — Wrapper-script approach for `wrapped-command` capabilities **Decision:** generate per-session wrapper script in `~/.platformctl-runtime/wrappers/&lt;session-id&gt;/&lt;command-name&gt;`. Wrapper: - shebang `#!/usr/bin/env bash` (POSIX, portable) - reads secret via Infisical CLI at exec time - exports specific env vars (declared in capability spec) - `exec` wrapped command **Rationale:** Python isn&#x27;t always on cousin host. Bash is. Wrapper is reviewable. Secret never persisted to disk. ### D11 — Cousin identity for low-tier requests = existing Forgejo PAT **Decision:** for `audit.level: low` capabilities (read-only repo status, list issues, etc.), no new session needed. Cousin uses existing Forgejo PAT from `security find-internet-password -s git.pdurlej.com -a &lt;cousin&gt; -w`. **Rationale:** AGENTS.md already establishes this PAT pattern. Don&#x27;t reinvent. Add audit log entry on use; that&#x27;s the discipline addition. ### D12 — `~/.platformctl-runtime/` is NOT sacred path **Decision:** `~/.platformctl-runtime/` MAY be deleted by operator at any time without data loss. Sessions revoke, agents stop, audit log preserved separately if needed. **Rationale:** sacred-path discipline applies to data-of-record (postgres volumes, vault snapshots, env files). Runtime state is by definition ephemeral. Allow operator to &quot;reset&quot; cleanly. --- ## Cross-reference matrix | Spec section | Source/related | |---|---| | Slice (a) catalog schema | `schema/module.schema.json` precedent (existing) | | Slice (b) audit log | ADR-0004 Iskra tool telemetry JSONL boundary | | Slice (c) Infisical resolver | `control-plane/platformctl/apply.py:_forgejo_token_from_infisical` | | Slice (c) ssh-agent | `runbooks/forgejo-actions-runner.md` (existing SSH discipline) | | Slice (d) approval | new — no existing precedent | | Constitution P5 wrapper rule | `docs/ci/runner-contract.md` runner-local capability pattern | | Hostname allowlist (FR-10) | issue #198 HOST-ALLOWLIST-01 | | Redaction (NFR-1, anti-pattern #6) | issues #199, #200 REDACT-COMMAND-01, REDACT-EXTEND-01 | | Cousin identity (ADR-0010) | every section | | Single status file (ADR-0006) | x.3 STATUS_NOW update task | | Risk-proportional canary (ADR-0007) | Codex execution notes in 03-tasks.md | | No stacked PRs (ADR-0017) | Codex execution notes — one slice = one PR | | Fix root causes, not workarounds (ADR-0018) | Acceptance criteria — no &quot;accept workaround&quot; outcomes | --- ## Composability proof This v0 must compose cleanly with future work: - **YubiKey approval (issue #132)** — slots into `approval.py` as a new backend in d.2. No core change. - **Codex→OpenClaw SSH (issue #73)** — first concrete capability; validates Slice (c). One catalog entry. - **Per-agent Forgejo/MCP identity split (issue #56)** — slots into `backings/` as a Forgejo PAT backing type. Reuses session machinery. - **Cloud-agent-fabric** — new backing type `remote-broker`; broker is itself an authorized cousin. No schema change. - **Operator-side break-glass via YubiKey** — `audit.level: critical` + YubiKey approval. Composable. If any future capability cannot express in this schema, the schema has a gap. Track as ADR. --- ## Known unknowns 1. **Infisical access from operator Mac** — does the Mac have direct Tailnet route to local Infisical, or does it need to go via rs2000 broker? Test in Slice (a) integration. 2. **Signal-cli identity for approval messages** — does the existing `home-platform-signal-cli` have a programmatic send API, or just CLI? Determines `approval.py` implementation. 3. **Forgejo notification permissions** — can an issue comment from `claude` cousin trigger a Signal notification for operator? Test in Slice (d). 4. **Lifetime of wrapper scripts** — at session expiry, wrapper scripts are removed. But what if cousin already started a wrapped command and the command is long-running? Wrapper-internal `exec` should survive ssh-agent expiry. Verify. 5. **Cross-cousin audit log queries** — `platformctl access audit --cousin codex` from claude&#x27;s session — is this allowed? D11 says own log read OK, cross forbidden. Operator only. --- ## Implementation order suggestion (codex pickup hint) If you (codex / claude / glm executing this v0) are picking up the work: 1. **Read this spec in order** (00 → 01 → 02 → 03 → 04 → README). Don&#x27;t skip 00 — it has the non-negotiables. 2. **Confirm scope with operator before starting Slice (a).** Capability schema is the foundation; getting it wrong cascades. 3. **Implement Slice (a) end-to-end + tests + canary review** before touching (b). Each slice is a PR; no stacks. 4. **Use existing precedents** (apply.py Infisical resolver, audit log JSONL pattern, ssh-agent runbook discipline). DRY &gt; novelty. 5. **Adversarial tests are MANDATORY** for Slice (d). If they pass with synthetic secrets, they prove nothing. Real cousin identity, real PAT, real ssh-agent. See d.6. 6. **Update this doc** as you implement. Decisions D13+, lessons learned, surprises. This is the durable record for the next cousin who reads it cold. </code></pre> </details> <details> <summary><code>docs/specs/agent-access-plane-v0/README.md</code></summary> <pre><code># Agent Access Plane v0 — Spec Kit **Phase:** 6 of 6 (index) | **Status:** draft — for review, NOT for merge **Source:** issue #76 (codex-authored) **Author:** claude (pre-impl scaffolding for codex pickup) **Pattern:** GitHub Spec Kit hybrid (constitution / specify / plan / tasks / impl-notes) **Precedent:** `pdurlej/iskra-openclaw` PR #276, #278, #279, #281 (same pattern, different repo) --- ## What&#x27;s in this directory | File | Phase | Purpose | |---|---|---| | [00-constitution.md](00-constitution.md) | 1 | Non-negotiable principles + forbidden anti-patterns | | [01-specify.md](01-specify.md) | 2 | Functional + non-functional requirements + open questions + falsifiable success criteria | | [02-plan.md](02-plan.md) | 3 | 4 slices (a/b/c/d), milestones, risks, rollback strategy | | [03-tasks.md](03-tasks.md) | 4 | Atomic task list, LOC estimates, codex execution notes | | [04-implement-notes.md](04-implement-notes.md) | 5 | Decisions log, cross-references, composability proof, codex pickup hints | | [README.md](README.md) | 6 | This file (index) | --- ## How to read this **If you are operator (piotr):** read this README + `00-constitution.md` § Principles + `01-specify.md` § Success Criteria. ~15 min total. Decide: accept/reject/amend the v0 scope. Operator-only signal. **If you are codex (executor):** read all 6 files in order. ~45 min. Then start Slice (a). Per `03-tasks.md` § Codex execution notes. **If you are claude (reviewer):** read all 6 files + cross-check against ADR-0003, ADR-0005, ADR-0010, ADR-0018. Flag drift. **If you are DeepSeek-v4-Pro (deep reviewer):** read all 6 files + assess composability claims in `04-implement-notes.md`. Look for blindspots the in-loop cousins missed. **If you are glm (cheap-pass reviewer):** read `00-constitution.md` + `02-plan.md` § Risks. Smell-check. --- ## TL;DR Agents (codex, claude, glm, ...) currently access platform credentials via per-cousin Forgejo PATs and ad-hoc shell environments. This works but creates exfiltration surface that scales with cousin count. **Agent Access Plane v0** introduces: - A **capability catalog** (`policies/agent-capabilities.yaml`) declaring what each cousin can request - A **session machinery** (`platformctl access`) that issues TTL-bounded, audit-logged capability handles - **Infisical-backed credential resolution** at the moment of use, never persisted to agent shell - **Operator approval** for high-audit capabilities, out-of-band (Signal / Forgejo / terminal) - **Wrapper scripts** for command capabilities, never leaking secrets to argv or env introspection **Cousin gets capability handle. Cousin never gets the credential.** v0 ships in 4 slices over ~4 weeks of cousin-execution time. No slice creates persistent state that survives revert. --- ## Status (this PR) | Surface | Status | |---|---| | Spec Kit (6 markdowns) | ✅ scaffold ready | | Code skeleton (`control-plane/platformctl/agent_access/`) | ✅ minimal scaffold (types, catalog, CLI stub) | | Schema (`schema/agent-capability.schema.json`) | ✅ draft | | Seed catalog (`policies/agent-capabilities.yaml`) | ✅ 5 example capabilities | | Tests | ⚠️ contract test scaffold only | | Slice (a) implementation | ❌ TODO (codex) | | Slice (b) implementation | ❌ TODO (codex) | | Slice (c) implementation | ❌ TODO (codex) | | Slice (d) implementation | ❌ TODO (codex) | **This PR is WIP draft. Not for merge.** Operator review of scope → operator approves → codex picks up Slice (a) in a new PR. --- ## Related issues - **#76** (parent) — Agent Access Plane product framing - **#73** — codex→OpenClaw SSH (first concrete capability use case) - **#56** — per-agent Forgejo/MCP identity split (composes with v0 via Forgejo PAT backing type) - **#72** — Forgejo Actions/canary Infisical integration (resolver pattern) - **#74** — repo-boundary decision (resolved by ADR-0003 + this spec) - **#132** — YubiKey approval (composes with v0 via approval backend) - **#198** — host allowlist (composes with v0 FR-10) - **#199, #200** — redaction (NFR-1 + Constitution anti-pattern #6) - **`pdurlej/iskra-openclaw#43`** — superseded by this v0; cross-repo coordination needed --- ## ADRs touched - **ADR-0003** (Agent Access Plane) — updated to reflect v0 actuals as part of x.1 - **ADR-0005** (coordination lanes) — referenced; not modified - **ADR-0007** (risk-proportional canary) — applied to Slice (a)-(d) PR tier classification - **ADR-0010** (cousin role taxonomy) — referenced; not modified - **ADR-0017** (no stacked PRs) — applied to Slice execution order - **ADR-0018** (fix root causes, not workarounds) — applied to acceptance criteria </code></pre> </details>
Collaborator

Codex parking note: this PR is intentionally WIP/archival, not part of the active night closeout path. Treat it as self-contained recovered knowledge from old branches. Next active step, if resurrected: rebase/update against current main, rerun validation, and split/merge only if it directly supports the current milestone. Because this touches agent capability/access policy, keep it security-sensitive/full-review until proven otherwise.

Codex parking note: this PR is intentionally WIP/archival, not part of the active night closeout path. Treat it as self-contained recovered knowledge from old branches. Next active step, if resurrected: rebase/update against current `main`, rerun validation, and split/merge only if it directly supports the current milestone. Because this touches agent capability/access policy, keep it security-sensitive/full-review until proven otherwise.
Collaborator

M10 disposition: moved to 10 - Improvements.

What this is: Agent capability catalog WIP.

Why parked here: Parked in M10 because it is a useful recovered WIP artifact for future agent-governance work, but remains not-ready/security-sensitive and not a closeout blocker.

This preserves the idea without letting it block M02/M03/M04 closeout. Before reactivation, split it into a narrow issue or PR with concrete acceptance criteria.

M10 disposition: moved to `10 - Improvements`. What this is: Agent capability catalog WIP. Why parked here: Parked in M10 because it is a useful recovered WIP artifact for future agent-governance work, but remains not-ready/security-sensitive and not a closeout blocker. This preserves the idea without letting it block M02/M03/M04 closeout. Before reactivation, split it into a narrow issue or PR with concrete acceptance criteria.
Collaborator

Triage verdict (claude): 🟢 MERGE-CANDIDATE (the useful 20%). dziadek correctly extracted the immediately-useful slice (capability catalog) from claude's 1000-line Agent Access Plane spec. Low-risk: it's policies/agent-capabilities.yaml + schema + types, no enforcement/runtime. One check before merge: verify the 7 seed capabilities match current cousin reality (claude/codex/iskra/gemini/oracle/dziadek/ollama lanes + the work-division seam #625). Then operator merge. This is the pragmatic head of #76 (held).

**Triage verdict (claude): 🟢 MERGE-CANDIDATE (the useful 20%).** dziadek correctly extracted the immediately-useful slice (capability catalog) from claude's 1000-line Agent Access Plane spec. Low-risk: it's `policies/agent-capabilities.yaml` + schema + types, no enforcement/runtime. One check before merge: verify the 7 seed capabilities match current cousin reality (claude/codex/iskra/gemini/oracle/dziadek/ollama lanes + the work-division seam #625). Then operator merge. This is the pragmatic head of #76 (held).
Collaborator

Verified + refreshed (claude, 2026-06-01). Checked the catalog against current cousin reality:

  • Added gemini (9th cousin, ADR-0023) to forgejo-pat-read + forgejo-pr-write — it was missing; gemini reviews PRs (canary 3+3 third opinion) like glm/deepseek.
  • The 7 SSH/secrets capabilities check out: codex deploy (rs2000), claude+codex vps1000-readonly + infisical-read, deepseek deep-review, operator break-glass.
  • 🔎 One nuance to flag (not auto-changed): claude/codex also do read-only host inspection of rs2000/vps1000 via the orchestrator-zone discipline (inherited operator SSH config, NOT a scoped Infisical credential — e.g. this session's agaria-pause inspection). Distinct from the scoped ssh-* capabilities here; mutation always needs an operator gate. If you want it formally cataloged (scoped + TTL'd) rather than inherited, that's a small follow-up — otherwise the catalog is accurate as a scoped-credential source of truth.

Now reflects 9 cousins → merge-ready (low-risk: catalog, no enforcement).

**Verified + refreshed (claude, 2026-06-01).** Checked the catalog against current cousin reality: - ✅ **Added gemini** (9th cousin, ADR-0023) to `forgejo-pat-read` + `forgejo-pr-write` — it was missing; gemini reviews PRs (canary 3+3 third opinion) like glm/deepseek. - ✅ The 7 SSH/secrets capabilities check out: codex deploy (rs2000), claude+codex vps1000-readonly + infisical-read, deepseek deep-review, operator break-glass. - 🔎 **One nuance to flag (not auto-changed):** claude/codex also do *read-only host inspection* of rs2000/vps1000 via the orchestrator-zone discipline (inherited operator SSH config, NOT a scoped Infisical credential — e.g. this session's agaria-pause inspection). Distinct from the scoped `ssh-*` capabilities here; mutation always needs an operator gate. If you want it formally cataloged (scoped + TTL'd) rather than inherited, that's a small follow-up — otherwise the catalog is accurate as a *scoped-credential* source of truth. Now reflects 9 cousins → merge-ready (low-risk: catalog, no enforcement).
policy(access): re-verify catalog 2026-06-01 — add gemini (9th cousin) to reviewer capabilities
All checks were successful
base-is-main / guard (pull_request) Successful in 1s
canary-required / collect-diff (pull_request) Successful in 3s
infra-docs-drift / docs-drift (pull_request) Successful in 4s
patchwarden-client-dry-run / collect-diff (pull_request) Successful in 3s
patchwarden-pr-sanity / collect-diff (pull_request) Successful in 3s
platformctl plan / auto-apply scope (pull_request) Successful in 17s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 15s
python-ci / Python 3.11 (pull_request) Successful in 35s
python-ci / Python 3.12 (pull_request) Successful in 36s
python-ci / Python 3.13 (pull_request) Successful in 35s
workflow-lint / lint (pull_request) Successful in 4s
canary-required / canary (pull_request) Successful in 12s
patchwarden-client-dry-run / dry-run (pull_request) Successful in 16s
patchwarden-pr-sanity / sanity (pull_request) Successful in 3m40s
2751d37d37
Collaborator

Patchwarden PR sanity

  • Status: advisory_findings
  • PR: 566
  • Commit: 2751d37d378eff60749609b309a1ffeae1c38567
  • Security-sensitive label: present
  • Authority: advisory model review plus deterministic blockers only
  • 3+3 canary: still alive; this does not replace it

Deterministic findings

  • info sensitive-path-touched Sensitive path touched — schema/agent-capability.schema.json
    • Evidence: schema/agent-capability.schema.json
    • Next: Route through the existing 3+3/risk-tier process; model review remains advisory.

Model reviewers

global-glm / glm-5.1:cloud

  • Status: ok

  • Verdict: OK

  • medium Schema permits ttl_default > ttl_max misconfiguration

    • Evidence: schema/agent-capability.schema.json lines 78-85 define ttl_default and ttl_max as independent integers with minimum:60 but no cross-field constraint. YAML entries like forgejo-pr-write (ttl_default:28800, ttl_max:86400) are valid, but nothi
    • Next: Add 'minimum' constraint note in schema description or implement custom validator enforcing ttl_default <= ttl_max when catalog is loaded
  • low Schema allows cousin_allowed/cousin_forbidden overlap without warning

    • Evidence: schema/agent-capability.schema.json lines 42-53 define cousin_allowed and cousin_forbidden as independent string arrays with no mutual exclusion. Current YAML (policies/agent-capabilities.yaml lines 56-58) keeps them disjoint, but future ed
    • Next: Add schema comment or documentation noting that cousin_forbidden takes precedence over cousin_allowed when both contain the same cousin

global-deepseek / deepseek-v4-pro:cloud

  • Status: ok

  • Verdict: OK

  • medium No validation of cousin names in capability catalog

    • Evidence: policies/agent-capabilities.yaml uses cousin names like 'claude', 'codex', 'deepseek', 'gemini', but neither the YAML schema nor the Python types enforce a closed set of valid cousins. A typo (e.g., 'deepseek-v4-pro' vs 'deepseek') would si
    • Next: Add an enum of valid cousin identifiers to schema/agent-capability.schema.json (e.g., via a top-level 'cousins' definition) and validate cousin_allowed/cousin_forbidden entries against it. Alternatively, enforce in CI via a custom validator.
  • medium Process discipline for high/critical capabilities is not enforced

    • Evidence: policies/agent-capabilities.yaml header comments state that changes to ssh-agent or audit.level high/critical require a Full tier PR, but there is no automated enforcement (e.g., CODEOWNERS, CI check) to block Lite-tier merges for such chan
    • Next: Implement a CI check (e.g., a script that parses the YAML and fails if a capability with audit.level in ['high','critical'] or backing.type == 'ssh-agent' is modified without a required reviewer or label) to prevent accidental weakening of access controls.
  • low Break-glass capability uses placeholder cousin name

    • Evidence: policies/agent-capabilities.yaml: 'break-glass-full-access' has cousin_allowed: [operator-only], which is not a real cousin agent. If the runtime later expects a valid cousin identifier, this could cause confusion or misconfiguration.
    • Next: Either document that 'operator-only' is a reserved sentinel value and handle it explicitly in the allows_cousin() logic, or replace with a real operator identity once the operator model is defined.

redteam / kimi-k2.6:cloud

  • Status: ok

  • Verdict: NOT_OK

  • high AuditEntry defaults outcome to success

    • Evidence: control-plane/platformctl/agent_access/_types.py: AuditEntrydataclass definesoutcome: str = 'success'. Any audit record instantiated without an explicit outcome will silently log a successful result, even for audit.level: critical c
    • Next: Remove the default and make outcome a required field (or default to 'unknown').

Policy notes

  • GLM 5.1 + DeepSeek V4 Pro are the operator-required model mix for this bot.
  • Optional red-team model is enabled only when PLATFORMCTL_PR_SANITY_REDTEAM_MODEL is configured.
  • Auto-merge is not enabled here.
<!-- patchwarden-pr-sanity:pdurlej/platform:PR-566 --> # Patchwarden PR sanity - Status: `advisory_findings` - PR: `566` - Commit: `2751d37d378eff60749609b309a1ffeae1c38567` - Security-sensitive label: `present` - Authority: advisory model review plus deterministic blockers only - 3+3 canary: still alive; this does not replace it ## Deterministic findings - **`info` `sensitive-path-touched`** Sensitive path touched — `schema/agent-capability.schema.json` - Evidence: `schema/agent-capability.schema.json` - Next: Route through the existing 3+3/risk-tier process; model review remains advisory. ## Model reviewers ### `global-glm` / `glm-5.1:cloud` - Status: `ok` - Verdict: `OK` - **`medium`** Schema permits ttl_default > ttl_max misconfiguration - Evidence: `schema/agent-capability.schema.json lines 78-85 define ttl_default and ttl_max as independent integers with minimum:60 but no cross-field constraint. YAML entries like forgejo-pr-write (ttl_default:28800, ttl_max:86400) are valid, but nothi` - Next: Add 'minimum' constraint note in schema description or implement custom validator enforcing ttl_default <= ttl_max when catalog is loaded - **`low`** Schema allows cousin_allowed/cousin_forbidden overlap without warning - Evidence: `schema/agent-capability.schema.json lines 42-53 define cousin_allowed and cousin_forbidden as independent string arrays with no mutual exclusion. Current YAML (policies/agent-capabilities.yaml lines 56-58) keeps them disjoint, but future ed` - Next: Add schema comment or documentation noting that cousin_forbidden takes precedence over cousin_allowed when both contain the same cousin ### `global-deepseek` / `deepseek-v4-pro:cloud` - Status: `ok` - Verdict: `OK` - **`medium`** No validation of cousin names in capability catalog - Evidence: `policies/agent-capabilities.yaml uses cousin names like 'claude', 'codex', 'deepseek', 'gemini', but neither the YAML schema nor the Python types enforce a closed set of valid cousins. A typo (e.g., 'deepseek-v4-pro' vs 'deepseek') would si` - Next: Add an enum of valid cousin identifiers to schema/agent-capability.schema.json (e.g., via a top-level 'cousins' definition) and validate cousin_allowed/cousin_forbidden entries against it. Alternatively, enforce in CI via a custom validator. - **`medium`** Process discipline for high/critical capabilities is not enforced - Evidence: `policies/agent-capabilities.yaml header comments state that changes to ssh-agent or audit.level high/critical require a Full tier PR, but there is no automated enforcement (e.g., CODEOWNERS, CI check) to block Lite-tier merges for such chan` - Next: Implement a CI check (e.g., a script that parses the YAML and fails if a capability with audit.level in ['high','critical'] or backing.type == 'ssh-agent' is modified without a required reviewer or label) to prevent accidental weakening of access controls. - **`low`** Break-glass capability uses placeholder cousin name - Evidence: `policies/agent-capabilities.yaml: 'break-glass-full-access' has cousin_allowed: [operator-only], which is not a real cousin agent. If the runtime later expects a valid cousin identifier, this could cause confusion or misconfiguration.` - Next: Either document that 'operator-only' is a reserved sentinel value and handle it explicitly in the allows_cousin() logic, or replace with a real operator identity once the operator model is defined. ### `redteam` / `kimi-k2.6:cloud` - Status: `ok` - Verdict: `NOT_OK` - **`high`** AuditEntry defaults outcome to success - Evidence: `control-plane/platformctl/agent_access/_types.py: `AuditEntry` dataclass defines `outcome: str = 'success'`. Any audit record instantiated without an explicit outcome will silently log a successful result, even for `audit.level: critical` c` - Next: Remove the default and make `outcome` a required field (or default to `'unknown'`). ## Policy notes - GLM 5.1 + DeepSeek V4 Pro are the operator-required model mix for this bot. - Optional red-team model is enabled only when `PLATFORMCTL_PR_SANITY_REDTEAM_MODEL` is configured. - Auto-merge is not enabled here.
pdurlej changed title from WIP: feat(agent-access): capability catalog — who can do what (seed v1) to feat(agent-access): capability catalog — who can do what (seed v1) 2026-06-02 15:24:46 +02:00
pdurlej deleted branch ollama/dziadek-agent-capability-catalog 2026-06-02 15:24:51 +02:00
Sign in to join this conversation.
No reviewers
No labels
W6d-automerge-calibration
agent/claude-code
agent/codex
agent/hermes
agent/iskra
agent/ollama
agent/patchwarden
automerge-candidate
class/security-sensitive
cutover-gate
dependency/blocked
dependency/blocks-others
dependency/cross-repo
dependency/needs-confirmation
domain:agents
domain:ci
domain:docs
domain:forgejo
domain:infra
domain:memory
domain:runtime
domain:signal
domain:ux
flow/architecture
flow/blocked
flow/deployed
flow/done
flow/implementation
flow/intake
flow/maintained
flow/observed
flow/ready
flow/refining
flow/retired
flow/review
iterating
judge/codex-candidate
judge/hermes-candidate
judge/low-confidence
judge/needs-refinement
judge/operator-needed
judge/p0
judge/p1
judge/p2
judge/p3
judge/park
judge/patchwarden-candidate
judge/stale-priority
kind/adr
kind/bug
kind/chore
kind/feature
kind/infra
kind/ops
kind/refactor
kind/research
large-impact
merge/auto
merge/manual
merge/manual-dependency-conflict
merge/manual-failing-tests
merge/manual-merge-conflict
merge/manual-missing-review
merge/manual-operator-preference
merge/manual-red-zone
merge/manual-security-sensitive
merge/manual-unclear-scope
merge/manual-unknown
meta
mode:operator-only
mode:patchwarden-iskra-approved
mode:safe-auto
needs-operator-decision
needs-triage
not-ready
observed/erroring
observed/needs-followup
observed/pending
observed/retire-candidate
observed/unused
observed/used
operator-emotional
owner-attention
phase/02
phase/03
priority:p0
priority:p1
priority:p2
priority:p3
proposed
ready-for-agent
ready-for-operator
recovery
review:claude-reviewed
review:codex-reviewed
review:dziadek-reviewed
review:needs-human
risk/exposure
risk/process
risk/product
risk/runtime
safety:external-write
safety:no-prod-mutation
safety:prod-impact
safety:secret-touch
size/large
size/medium
size/small
size/tiny
size/unknown
source/adr
source/agent-generated
source/manual
source/operator-chat
source/voice-note
status:blocked
status:codex-ready
status:merged:pending-evidence
status:needs-evidence
status:operator-needed
status:parked
tier/full
tier/lite
tier/stacked
tier:0-platform-substrate
tier:1-iskra-value-layer
tier:2-tools-products-modules
type:bug
type:chore
type:docs
type:feat
type:policy
type:research
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pdurlej/platform!566
No description provided.