Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions crates/temperpaw/tests/paw_fs_hot_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,49 @@ fn monty_exposes_write_many_for_artifact_sets() {
);
}

#[test]
fn genesis_publish_git_commands_are_noninteractive_and_authenticated() {
let source = repo_file("os-apps/paw-agent/wasm/monty_repl/src/dispatch.rs");
assert!(
source.contains("genesis_registry_auth_header"),
"publish_app/update_app must derive an Authorization extraHeader for Genesis git smart HTTP"
);
assert!(
source.contains("Authorization: Bearer"),
"Genesis git auth should use the GitToken bearer contract accepted by genesis git_auth"
);
assert!(
source.contains("GIT_TERMINAL_PROMPT=0"),
"Genesis git publish must fail deterministically instead of prompting agents for username/password"
);
}

#[test]
fn pawfs_single_entity_lookups_are_bounded_to_one_result() {
let monty_source = repo_file("os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs");
for (collection, helper) in [
("Directories", "find_pawfs_directory"),
("Files", "find_pawfs_file"),
] {
assert!(
monty_source.contains(&format!("/tdata/{collection}?$filter={{filter}}&$top=1")),
"{helper} must bound direct PawFS lookups with $top=1 so hot-path writes cannot trip OData QueryTooLarge on duplicate/stale rows"
);
}
assert!(
monty_source.contains("pawfs_stable_entity_id(\"dr\", ws_id, &path)")
&& monty_source.contains("is_pawfs_lookup_too_large(&error)")
&& monty_source.contains("\"Id\": directory_id"),
"Directory creation must fall back to stable idempotent creates when path lookups still trip QueryTooLarge"
);

let artifact_batch_source = repo_file("os-apps/paw-fs/wasm/artifact_batch_apply/src/lib.rs");
assert!(
artifact_batch_source.contains("/tdata/{set_name}?$filter={encoded}&$top=1"),
"artifact_batch_apply single-entity lookup must also be bounded with $top=1"
);
}

#[test]
fn default_agent_tool_allowlists_include_write_many() {
for path in [
Expand Down
10 changes: 10 additions & 0 deletions crates/temperpaw/tests/session_turn_architecture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,16 @@ fn session_entry_readbacks_stay_within_bounded_query_budget() {
helpers.contains("session_entries_verify_urls"),
"batched SessionEntry readback should use one bounded per-entry URL per expected entry"
);
assert!(
helpers.contains("$orderby=Sequence%20asc,EntryId%20asc"),
"SessionEntry context reads should use deterministic bounded pages instead of an unordered session-wide scan"
);
assert!(
helpers.contains("read_session_from_entries_chain")
&& helpers.contains("session_leaf_id")
&& helpers.contains("ParentEntryId"),
"SessionEntry context reads with a known leaf should walk bounded per-entry parent links instead of listing the whole session"
);
assert!(
route_message.contains("session_leaf_id is missing; starting clean continuation"),
"route_message should start cleanly instead of broad-scanning when the prior leaf hint is missing"
Expand Down
57 changes: 57 additions & 0 deletions docs/adrs/0066-agent-genesis-git-publish-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ADR-0066: Agent Genesis Git Publish Auth

- Status: Proposed
- Date: 2026-06-18
- Deciders: TemperPaw maintainers

## Context

TemperPaw agents repair installed apps by editing a workspace copy, calling
`temper.publish_app(...)` or `temper.update_app(...)`, then installing the
returned pinned Genesis ref. The registry-side OData actions can be reached with
the existing agent headers, but Genesis smart HTTP git receive-pack rejects
anonymous pushes.

The previous publish path configured only `X-Tenant-Id` as a git extraHeader.
When Genesis required authentication, agents saw interactive git credential
failures such as "could not read Username" instead of a deterministic tool error.
That made app repairs easy to leave as local-only edits.

Genesis already accepts active GitToken secrets through `Authorization: Bearer`
on git smart HTTP requests.

## Decision

`temper.publish_app(...)` and `temper.update_app(...)` will resolve a Genesis
GitToken from a Temper secret and send it to git as an extraHeader:

- default secret names, tried in order: `GENESIS_GIT_TOKEN`, then the existing
production-compatible `GENESIS_TOKEN`
- per-call overrides: `registry_token_secret`, `git_token_secret`,
`genesis_git_token_secret`, `genesis_token_secret`, or `RegistryTokenSecret`
- agent config overrides: `genesis_registry_token_secret`,
`GENESIS_REGISTRY_TOKEN_SECRET`, `genesis_git_token_secret`, or
`GENESIS_GIT_TOKEN_SECRET`, `genesis_token_secret`, or `GENESIS_TOKEN_SECRET`

The git command also sets `GIT_TERMINAL_PROMPT=0` so missing or invalid auth
fails immediately with explicit evidence.

The token is not returned in tool results. Successful publish/update results
continue to return the pinned `owner/name@hash` ref and verified Genesis latest
hash metadata.

## Consequences

- Agent app repairs can push to Genesis without human credential prompts.
- Missing publish credentials fail before a local-only change can be mistaken
for an installed repair.
- Deployments must provision an active Genesis GitToken in the configured Temper
secret before agents can publish or update apps.
- The existing install step remains mandatory: pushing a new Genesis version is
not the same thing as installing that pinned ref into a tenant.

## Non-Goals

- This ADR does not change Genesis GitToken validation semantics.
- This ADR does not add password-based git credentials.
- This ADR does not make unpinned app installs acceptable for agents.
4 changes: 4 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ repo-local app catalog. Configure fresh-instance bootstrap with pinned refs:
- `TEMPERPAW_GENESIS_BOOTSTRAP_REFS=temperpaw/paw-fs@HASH,temperpaw/paw-agent@HASH`
- `TEMPERPAW_GENESIS_CACHE_RESTORE_TIMEOUT_SECS=20`
- `TEMPERPAW_GENESIS_BOOTSTRAP_TIMEOUT_SECS=60`
- Temper secret `GENESIS_GIT_TOKEN` or `GENESIS_TOKEN`, or
`GENESIS_GIT_TOKEN_SECRET` / `GENESIS_TOKEN_SECRET` pointing at an equivalent
active Genesis GitToken secret, for agent `publish_app` and `update_app` git
pushes.

On restart with the same database, TemperPaw restores installed Genesis app
metadata and skips unchanged bootstrap refs. Do not reset, wipe, replace, or
Expand Down
204 changes: 204 additions & 0 deletions docs/proofs/2026-06-18-katagami-publish-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Katagami Publish Path E2E Proof

- Date: 2026-06-18
- Linear: ARN-51, ARN-52, ARN-54, ARN-55, ARN-57
- TemperPaw PR: https://github.com/nerdsane/temperpaw/pull/415
- Genesis PR: https://github.com/arni-labs/genesis/pull/36

## Scope

This proof covers the Katagami curation artifact publishing path after fixing
the app publish/install path, Genesis receive-pack auth support, PawFS hotload
writes, the Ready File invariant, verification evidence, and installed app
provenance.

The final production verifier required more than a successful agent response:
the session had to create a `Files` entity in workspace `katagami` with
`Status=Ready`, and the bytes read from `/tdata/Files('{id}')/$value` had to
match the expected SHA-256.

## Issues Found

1. Genesis receive-pack could not accept an authenticated app publish when the
pushed pack referenced a base object that existed only in the server object
database. This blocked publishing the updated `temperpaw/paw-agent` app.
2. `context_preparer` still used broad `SessionEntry` collection reads in hot
sessions, which hit `HTTP 413 QueryTooLarge` in production.
3. The first-turn virtual SessionEntry leaf was queried as if it already
existed durably, which caused another bounded-query 413 on the virtual
`u-{session_id}-0` leaf.
4. PawFS directory lookup for `/proofs` used an exact filtered collection read
that also hit `HTTP 413 QueryTooLarge`; the agent reported done, but no Ready
File existed.
5. The proof file and Linear state still described the old blocked state instead
of the live installed app and Ready File evidence.
7. OpenPaw bootstrap refs could drift back to the older `paw-agent` app hash
after restart unless the pinned hash was updated.

No duplicate Linear issues were created; updates were added to the existing
issues, primarily ARN-54 for the bounded-query PawFS hotload blocker.

## Fixes

### Genesis

- Commit: `4e48827 Fix receive-pack delta base fallback`
- PR: https://github.com/arni-labs/genesis/pull/36
- Deployment: `fa798fd2-a606-47ea-b1a8-b3ec0ac0b083`
- Image digest:
`sha256:98e07c2ffb49b49000f798e53bf19ab12b98873e8cf7941ae955084620ad487c`
- Result: receive-pack accepts the authenticated publish path used for
`temperpaw/paw-agent`.

### TemperPaw

- `66cd238a300cfe8601a29435097024dac620bb39`
`Fix Genesis publish auth and bounded PawFS lookups`
- `b09358cf` `Bound SessionEntry context reads`
- `da31ed64` `Walk SessionEntry chains by leaf`
- `21fa21d4` `Skip OData for virtual first turn leaf`
- `97dff829` `Fallback PawFS directory creates on query budget`

Key behavior changes:

- `wasm-helpers` now supports bounded SessionEntry chain reads from a known
`session_leaf_id`.
- The virtual first-turn leaf `u-{session_id}-0` is recognized without issuing
an OData lookup.
- `context_preparer` passes the session leaf into the helper instead of
falling back to a broad session read.
- PawFS directory creation now uses stable path-scoped entity ids and treats
query-budget lookup failures as a signal to perform idempotent create.

## Local Verification

- Red/green:
`cargo test -p temperpaw --test session_turn_architecture session_entry_readbacks_stay_within_bounded_query_budget -- --nocapture`
- Red/green:
`cargo test virtual_first_turn_leaf_is_detected_without_odata_probe -- --nocapture`
- Red/green:
`cargo test pawfs_stable_directory_ids_are_path_scoped -- --nocapture`
- Full focused tests:
`cargo test -p temperpaw --test session_turn_architecture -- --nocapture`
- Result: 24 passed.
- Full helper tests:
`cargo test -- --nocapture`
- Path: `os-apps/paw-agent/wasm/wasm-helpers`
- Result: 39 passed.
- PawFS hot path tests:
`cargo test -p temperpaw --test paw_fs_hot_path -- --nocapture`
- Result: 15 passed.
- Monty REPL PawFS tests:
`cargo test pawfs_ -- --nocapture`
- Path: `os-apps/paw-agent/wasm/monty_repl`
- Result: 5 passed.
- Formatting:
`cargo fmt --check`
- Result: passed.
- WASM build:
`./build.sh`
- Path: `os-apps/paw-agent/wasm`
- Result: passed with pre-existing warnings in unrelated modules.

## Publish And Install Evidence

Final published `paw-agent` app hash:

- `temperpaw/paw-agent@18f58e340795e66015e428534679222eb2afaced`

Genesis readback:

- `LatestVersionHash=18f58e340795e66015e428534679222eb2afaced`
- `NewHash=18f58e340795e66015e428534679222eb2afaced`
- `RefName=main`
- `Status=Active`

Fresh Genesis clone evidence:

- `clone_head=18f58e340795e66015e428534679222eb2afaced`
- `git fsck --full`: ok
- tracked WASM count: 21
- PawFS stable directory guard present in cloned `wasm/monty_repl` source.

OpenPaw install evidence:

- Install API: `HTTP 200`
- Installed app ref:
`temperpaw/paw-agent@18f58e340795e66015e428534679222eb2afaced`

Production installed app provenance:

- `paw-agent`
- `source_kind=genesis`
- `version_hash=18f58e340795e66015e428534679222eb2afaced`
- `pinned_version_hash=18f58e340795e66015e428534679222eb2afaced`
- `current_version_hash=18f58e340795e66015e428534679222eb2afaced`
- `follow_policy=pinned`
- `status=installed`
- `paw-fs`
- `8cc9c1a0c3959ba0555a6eac5446db76de747817`
- `katagami-curation`
- `1e6f43993be70ca3d7dadf42c032fa6a206ac482`

Bootstrap persistence:

- `TEMPERPAW_GENESIS_BOOTSTRAP_REFS` now points `temperpaw/paw-agent` at
`18f58e340795e66015e428534679222eb2afaced`.

OpenPaw deployment:

- Deployment: `2545e028-efe2-4700-9e07-7072a73a0076`
- Status: `SUCCESS`
- `/healthz`: HTTP 200
- `/readyz`: HTTP 200

## Failed Live Attempts During Diagnosis

- `ss-hotload-e2e-20260618043245`
- Failed in `context_preparer`.
- Error: `SessionEntry list failed (HTTP 413): QueryTooLarge`.
- `ss-hotload-e2e-20260618045510`
- Failed while reading the virtual first-turn leaf.
- Error: `SessionEntry chain read failed for EntryId=u-...-0 (HTTP 413): QueryTooLarge`.
- `ss-hotload-e2e-20260618050218`
- Session completed with `(done)`, but independent file verification failed.
- SessionEntry forensic evidence showed `temper.write` failed on directory
lookup:
`GET /tdata/Directories?$filter=Path eq '/proofs' and WorkspaceId eq 'katagami' ...`
returned `413 QueryTooLarge`.
- Exact Ready File query returned zero rows.

## Final Live E2E

Production session:

- Session: `ss-hotload-e2e-20260618051545`
- Proof path: `/proofs/katagami-hotload-e2e-20260618051545.md`
- Workspace: `katagami`
- Runtime app hash:
`temperpaw/paw-agent@18f58e340795e66015e428534679222eb2afaced`
- Session final status: `Completed`
- Session result: `(done)`

Independent Ready File verification:

- Ready file count: 1
- File id: `fl-019ed928-b94c-7953-adaa-37981496cad3`
- Expected SHA-256:
`ce32628b7cd1c561ca7a23401a1e3998c1c0820f4b23150bfbafb41ec1033e70`
- Actual SHA-256:
`ce32628b7cd1c561ca7a23401a1e3998c1c0820f4b23150bfbafb41ec1033e70`
- Result: `ready_file_content_match=true`

## ADR Note

No new ADR was added for the final two TemperPaw code changes because they are
bounded implementation corrections to already-accepted SessionEntry and PawFS
hot-path architecture decisions:

- bounded SessionEntry reads are covered by the existing SessionEntry hot-path
ADR set;
- stable PawFS directory creation preserves the existing entity model and only
makes the create path idempotent when the exact lookup exceeds query budget.

The judgement is recorded here and should also be linked from the PR notes.
5 changes: 5 additions & 0 deletions os-apps/paw-agent/system/skills/platform-awareness/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ new_ref = temper.update_app({
temper.install_app({"app_ref": new_ref, "follow_policy": "pinned", "reason": "Roll forward repaired app"})
```

`publish_app` and `update_app` require a configured Genesis GitToken secret
(`GENESIS_GIT_TOKEN` or `GENESIS_TOKEN` by default, or an explicit
`registry_token_secret`). Missing auth is a publish blocker; it is not a
successful repair until the returned pinned ref is installed and verified.

Then verify the entity/action that was broken. Report the old pinned ref, the
new pinned ref, and the smoke result. A normal app repair is a new version of
the same Genesis app; it is not a fork or lineage change unless you are creating
Expand Down
5 changes: 5 additions & 0 deletions os-apps/paw-agent/system/skills/temper-app-creation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ new_ref = temper.update_app({
temper.install_app({"app_ref": new_ref, "follow_policy": "pinned", "reason": "Install repaired app"})
```

`publish_app` and `update_app` use the configured Genesis GitToken secret
(`GENESIS_GIT_TOKEN` or `GENESIS_TOKEN` by default, or `registry_token_secret`
when explicitly provided). If auth is missing, stop and report the failed
publish instead of claiming the local edit is installed.

Verify the broken entity/action after install. A repair is a new version of the
same Genesis app. Use lineage only when creating a fork/import/derivative app.

Expand Down
12 changes: 10 additions & 2 deletions os-apps/paw-agent/wasm/context_preparer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,15 +499,17 @@ fn read_session_from_temperfs(
temper_api_url: &str,
tenant: &str,
file_id: &str,
session_leaf_id: Option<&str>,
) -> Result<String, String> {
if wasm_helpers::is_session_entries_ref(file_id) {
let fields = ctx.entity_state.get("fields").cloned().unwrap_or(json!({}));
return wasm_helpers::read_session_from_temperfs(
return wasm_helpers::read_session_from_temperfs_with_leaf(
ctx,
temper_api_url,
tenant,
&fields,
file_id,
session_leaf_id,
);
}

Expand Down Expand Up @@ -1213,7 +1215,13 @@ fn load_messages_for_prepare(
let use_session_tree = !session_file_id.is_empty() && !session_leaf_id.is_empty();
if use_session_tree {
let session_jsonl =
read_session_from_temperfs(ctx, temper_api_url, tenant, session_file_id)?;
read_session_from_temperfs(
ctx,
temper_api_url,
tenant,
session_file_id,
Some(session_leaf_id),
)?;
if session_jsonl.is_empty() {
if is_session_entries_ref(session_file_id) {
ctx.log(
Expand Down
Loading
Loading