diff --git a/harnesses/contexts/cp-docs-token/v1/CLAUDE.md b/harnesses/contexts/cp-docs-token/v1/CLAUDE.md new file mode 100644 index 0000000..3ca223c --- /dev/null +++ b/harnesses/contexts/cp-docs-token/v1/CLAUDE.md @@ -0,0 +1,48 @@ +# Step documents (cp-docs-token) + +This step can read and write planner documents through `python3 -m lib.cp_cli` +in step-docs mode (PLN-CLI-16). The mode is active because `CP_DOCS_TOKEN_FILE` +and `CP_DOCS_CA_FILE` are set and the listener URL is in `CP_DOCS_URL`. + +## Credential + +- The token is a file at `$CP_DOCS_TOKEN_FILE`. cp-cli reads it on every call. +- Never print, copy, log or pass the token on a command line. +- Do not use `~/.cpconfig`; step-docs mode does not read it. + +## Allowed commands + +Only these commands work; anything else exits 2 `cp_docs_mode_command_not_allowed`: + +``` +python3 -m lib.cp_cli planner items documents list +python3 -m lib.cp_cli planner items documents get [--json] +python3 -m lib.cp_cli planner items documents put --file (--create-only | --if-match ) +python3 -m lib.cp_cli planner items documents related [--json] +``` + +Item arguments are full UUIDs. Do not pass a project id. + +## Scope + +- You may write only documents on this step's own item. +- You may read documents on this step's item, its parent and its siblings. +- `related` prints `item_uuid`, `parent_uuid`, `sibling_uuids` and `siblings_truncated`. + +## Writes need a precondition + +Exactly one of these is required on every `put` (neither or both exits 2): + +- `--create-only` creates a new document (sends `If-None-Match: *`). +- `--if-match ` updates an existing one (sends `If-Match`). Take `` + from `get --json` (the ETag revision); surrounding quotes are stripped. + +## Errors + +- 412 `precondition_failed`: the document changed or already exists. Re-run + `get --json`, merge your change into the current content, and `put` again + with the new revision. +- 428 (precondition missing): add `--create-only` or `--if-match`. +- 429 `step_docs_write_budget_exhausted` or `step_docs_rate_limited`: stop + writing; do not retry in a loop. +- 404 `not_found`, 400 `invalid_document_name`: fix the item uuid or name. diff --git a/harnesses/contexts/cp-docs-token/v1/harness.yaml b/harnesses/contexts/cp-docs-token/v1/harness.yaml new file mode 100644 index 0000000..ace0a9b --- /dev/null +++ b/harnesses/contexts/cp-docs-token/v1/harness.yaml @@ -0,0 +1,16 @@ +kind: context +name: cp-docs-token +version: 1 +description: "Step-docs mode env contract for lib.cp_cli (PLN-CLI-16): per-step wfcd_ token file and CA file delivered by the dispatcher; document access scoped to this step's item (spec/dispatcher.md D-40, spec/authentication.md AU-59)" +requires: [] +provides: [cp-docs-token] + +# Env-only layer: the dispatcher delivers the token and CA files into the +# container; this layer sets only the paths. No secrets_required, no init script. +env: + CP_DOCS_TOKEN_FILE: /run/agent/cp-docs/token + CP_DOCS_CA_FILE: /run/agent/cp-docs/ca.crt + +context_files: + - source: ./CLAUDE.md + target: /opt/harness/context/cp-docs-token/CLAUDE.md diff --git a/task-templates/workflow/frozen-fixture-emit@1.yaml b/task-templates/workflow/frozen-fixture-emit@1.yaml index c827171..c8fa515 100644 --- a/task-templates/workflow/frozen-fixture-emit@1.yaml +++ b/task-templates/workflow/frozen-fixture-emit@1.yaml @@ -24,7 +24,6 @@ nodes: kind: script cmd: scripts/workflow/state-tx cp_write: true - on_fail: continue runtime_env: CP_URL: "https://agents.oreillyit.nz/api" STATE_TX_NEW_STATE: test-skeleton-ready diff --git a/task-templates/workflow/test-validate@1.yaml b/task-templates/workflow/test-validate@1.yaml index 38fec1f..70205f2 100644 --- a/task-templates/workflow/test-validate@1.yaml +++ b/task-templates/workflow/test-validate@1.yaml @@ -29,12 +29,10 @@ nodes: kind: script cmd: scripts/workflow/preflight-test-fail-mode cp_write: false - on_fail: continue - id: state-tx kind: script cmd: scripts/workflow/state-tx cp_write: true - on_fail: continue runtime_env: CP_URL: "https://agents.oreillyit.nz/api" STATE_TX_NEW_STATE: test-validated diff --git a/tests/invariants/test_cp_docs_token_layer.py b/tests/invariants/test_cp_docs_token_layer.py new file mode 100644 index 0000000..bd122c1 --- /dev/null +++ b/tests/invariants/test_cp_docs_token_layer.py @@ -0,0 +1,56 @@ +"""cp-docs-token/v1 context: env-only step-docs layer (PLN-CLI-16, D-40, AU-59). + +The per-step wfcd_ token and CA file are delivered by the dispatcher. The layer +sets the env contract and ships one context doc; no ESO mount, no init script, +and no reference to the cp-service-token layer. +""" +from __future__ import annotations + +from pathlib import Path + +import yaml + +_LAYER = ( + Path(__file__).resolve().parents[2] / "harnesses" / "contexts" / "cp-docs-token" / "v1" +) + + +def _doc() -> dict: + return yaml.safe_load((_LAYER / "harness.yaml").read_text()) + + +def test_cp_docs_token_layer_parses_with_env_keys(): + doc = _doc() + assert doc["kind"] == "context" + assert doc["name"] == "cp-docs-token" + assert doc["version"] == 1 + assert doc["requires"] == [] + assert doc["provides"] == ["cp-docs-token"] + assert doc["env"] == { + "CP_DOCS_TOKEN_FILE": "/run/agent/cp-docs/token", + "CP_DOCS_CA_FILE": "/run/agent/cp-docs/ca.crt", + } + + +def test_cp_docs_token_layer_declares_no_secrets_and_no_init(): + doc = _doc() + assert not doc.get("secrets_required") + assert not doc.get("scripts") + assert not (_LAYER / "init.sh").exists() + + +def test_cp_docs_token_layer_context_file_mapping(): + doc = _doc() + assert doc["context_files"] == [ + { + "source": "./CLAUDE.md", + "target": "/opt/harness/context/cp-docs-token/CLAUDE.md", + } + ] + assert (_LAYER / "CLAUDE.md").is_file() + + +def test_cp_docs_token_layer_never_mentions_cp_service_token(): + for path in _LAYER.rglob("*"): + if path.is_file(): + assert "cp-service-token" not in path.read_text(), path