Skip to content

feat(cli): expose workdir-derived hashes via omac diagnose --hash - #266

Open
Retr0MrWave wants to merge 2 commits into
mainfrom
feat/diagnose-hash
Open

feat(cli): expose workdir-derived hashes via omac diagnose --hash#266
Retr0MrWave wants to merge 2 commits into
mainfrom
feat/diagnose-hash

Conversation

@Retr0MrWave

Copy link
Copy Markdown
Contributor

Issue: Closes #156

What

  • omac diagnose --hash[=runtime|serve|keychain|cache|all] prints the four identifiers omac derives from a workdir, in text or --json.
  • The hashing that createRuntimeDir/createRuntimeDirServe inlined moves to internal/cli/runtimedir.go; both creators now call it. One sha256 of a workdir left in the package.
  • describeCacheScope lifted out of provenance's buildCacheView, so --hash=cache and omac provenance cannot disagree.

Why

Four different workdir-derived identifiers, none reachable from the CLI — debugging a run meant reverse-engineering a log path or a cache directory by hand. internal/cli/controlinfo.go:18-20 states the gap outright.

Shape follows the comment on #156 (a debugging flag, not a new top-level verb) rather than the issue body's omac hash. It mirrors the existing --probe compute-and-exit precedent.

How

The issue's hard requirement is single-source-of-truth, and that is what forced the refactors:

  • createRuntimeDir / createRuntimeDirServe os.RemoveAll their target, so a reporting path that called them would wipe a live session's logs/ and pids/. Only the pure derivation moved out; the creators keep their side effects.
  • Every value is read from the producer the runtime uses — runtimeDirPath, serveRuntimeDirPath, keychain.WorkdirID, describeCacheScopetoolcache.Describe*. Nothing here recomputes a digest.

Sandbox correctness (2nd commit, from self-review): start.go:1011 / serve.go:1499 export TMPDIR=<sandboxTmp> to the inner agent, so joining os.TempDir() handed an in-sandbox agent a path that does not exist — precisely the audience docs/NONO_SANDBOX.md points at. $OMAC_SOCKET is unaffected (socketPath = filepath.Join(rtDir, "bridge.sock"), start.go:713), so its parent is read as the live runtime dir when it names the same directory as the derived one. Guarded on the dir name, so another workdir's session — or a start session while asking for serve — cannot hijack the answer.

Two deliberate deviations from the issue body, both worth a look:

  • JSON is a stable envelope ({workdir, tmpdir, entries[]}), not the issue's flat {...,"dir":...}. Single-kind scripting is jq -r '.entries[0].path'. tmpdir is load-bearing given the remap above.
  • Text output is stanzas, not a table — the keychain and cache digests are full 64-hex, which pads a KIND/DIGEST/PATH table past 140 columns.

Verification

$ gofmt -l . && go vet ./... && python3 scripts/check-docs.py
[ok] README.md 233/400 lines; all doc links resolve

$ go test -race -count=1 ./internal/cli/ ./internal/toolcache/ ./internal/keychain/
ok  github.com/tngtech/oh-my-agentic-coder/internal/cli        7.276s
ok  github.com/tngtech/oh-my-agentic-coder/internal/toolcache  2.108s
ok  github.com/tngtech/oh-my-agentic-coder/internal/keychain   1.012s

17 tests in hash_test.go, all passing. They assert against the real producers, not hardcoded digests — the drift guard the issue asks for. Mutation-checked: re-inlining a different hash inside createRuntimeDir fails TestDiagnoseHashRuntimeMatchesCreateRuntimeDir with a clear message. (Mutating the shared helper correctly does not fail; both sides move together, which is the point.)

Manual, against the built binary:

# cache path agrees with the already-shipped provenance surface (default and workdir scope)
$ diff <(omac diagnose --hash=cache --json | jq -r '.entries[0].path') \
       <(omac provenance --json | jq -r '.cache.path')          # MATCH

# in-sandbox case: TMPDIR remapped, OMAC_SOCKET set -> live dir, and the doc's command resolves
$ TMPDIR=/tmp/omac-sandbox-tmp-fake OMAC_SOCKET="$REAL/bridge.sock" \
    sh -c 'ls "$(omac diagnose --hash=runtime --json | jq -r .entries[0].path)/logs"'
echo-rest.log

Error paths exercised by hand and by test: unknown kind, the --hash runtime space form, a stray non-kind argument, and an unresolvable cache scope in both all mode (stderr + other kinds survive, exit 0) and single-kind mode (stderr, exit 5).

Not green locally, and not from this branch: TestIntegrationWorktreeKnownLimitations (git packed-refs: Device or resource busy) and TestIntegrationWorkflowInterpretersRunnable (bwrap: execvp node, node at a non-standard path) fail in internal/sandboxrun. Confirmed via git stash that both fail identically on a clean main. sandboxrun does not import internal/cli.

Follow-up

  • Two commits kept separate so the self-review trail is legible — squash on merge if you prefer one.
  • clearActiveScope (internal/cli/cache.go:86-96) has a structurally similar scope switch, but it dispatches to toolcache.Clear* rather than Describe*; folding it in would mean changing toolcache's public API, so it is deliberately left alone.
  • Sandbox cache isolation breaks harness plugin persistence across workdirs (cold cache / offline) #143 may split the cache into two scopes; --hash=cache should then expose both.

🤖 Generated with Claude Code

Ilia Zhuravok and others added 2 commits August 25, 2026 13:54
Refs #156

omac derives four different identifiers from a workdir -- the start and
serve runtime dirs, the keychain secret scope, and the tool-cache scope --
and none of them was reachable from the CLI, so debugging a run meant
reverse-engineering a log path or a cache directory by hand.

Every value is read from the producer the runtime itself uses; nothing
here recomputes a digest. That is what required the two refactors:

  - createRuntimeDir / createRuntimeDirServe inlined their sha256 inside
    functions that also os.RemoveAll their target, so a reporting path
    that called them would wipe a live session's logs and pids. The pure
    name derivations move to runtimedir.go and both creators call them.
  - describeCacheScope is lifted out of provenance's buildCacheView, so
    --hash=cache and `omac provenance` cannot disagree about which cache
    a workdir resolves to.

Shape follows the issue's comment (a debugging flag, not a new top-level
verb) and mirrors the existing --probe compute-and-exit precedent. Two
deviations from the issue body: JSON is a stable envelope with a tmpdir
field rather than a flat object, because the runtime/serve paths are
relative to $TMPDIR and a shell whose TMPDIR differs from the agent's
resolves a different, equally correct path; and text output is stanzas
rather than a table, because the full 64-hex digests pad a table past
140 columns.

The tests assert against the real producers rather than hardcoded
digests, which is the drift guard the issue asks for: re-inlining a hash
in createRuntimeDir fails them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ilia Zhuravok <ilia.zhuravok@tngtech.com>
Refs #156

Review findings on the previous commit.

The important one: os.TempDir() is the wrong answer in the one place the
answer matters most. start.go and serve.go export TMPDIR=<sandboxTmp> to
the inner agent, so an agent running `omac diagnose --hash=runtime` from
inside the sandbox -- the audience the NONO_SANDBOX.md pointer was aimed
at -- was handed <sandboxTmp>/omac-<hash>, which does not exist. The
digest was right; only the join was wrong. $OMAC_SOCKET is unaffected
(both modes set it to <rtDir>/bridge.sock on the host), so its parent is
read as the live runtime dir when it names the same directory as the
derived path. A session belonging to another workdir, or a start session
while asking for the serve kind, does not hijack the result.

Also:
  - Failed kinds in --hash=all wrote an "error" stanza to stdout and
    exited 0, so a caller scraping stdout for paths could pick it up.
    Text-mode errors now go to stderr; the JSON `error` field is
    unchanged, being structured.
  - `--hash somethingelse` advised "use --hash=somethingelse", which
    fails again with "unknown kind". The attached-form hint is now gated
    on the stray token actually being a kind.
  - CLI.md claimed the cache kind always hashes "v1:<domain>:<canonical>"
    and a symlink-resolved path; under the default global scope it hashes
    the constant "v1:shared" with no path at all, so two projects show
    identical digests. Documented per scope.
  - CLI.md claimed --hash=cache reports "the cache this workdir actually
    gets". Like provenance, it reports the CONFIG-resolved scope and does
    not see --cache-scope, --ephemeral-cache, or --no-sandbox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ilia Zhuravok <ilia.zhuravok@tngtech.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): expose the workdir hash via an omac hash command

1 participant