Skip to content

refactor(approval-gate): port to idiomatic TypeScript - #161

Merged
ytallo merged 7 commits into
mainfrom
refactor/approval-gate-typescript-idioms
May 20, 2026
Merged

refactor(approval-gate): port to idiomatic TypeScript#161
ytallo merged 7 commits into
mainfrom
refactor/approval-gate-typescript-idioms

Conversation

@ytallo

@ytallo ytallo commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

harness-node's approval-gate was a literal Rust port: hand-rolled typeof validation, as Record<string, unknown> casts at every boundary, Promise<unknown> handler returns, mutable redaction loops, and inline composite-key string slicing. The rest of harness-node had already moved to Zod schemas + inferred types + JSON-schema export. This PR brings the approval-gate and the harness policy module up to that bar, with no change to the on-the-wire contract.

approval-gate

  • schemas.ts (new) — Zod schemas, inferred types, and a derived JSON schema for approval::resolve. Houses the wire schemas, key helpers (pendingKey / approvalResumeFnId), parsePolicyReply, and STATE_SCOPE. ResolvePayloadSchema accepts function_call_id or the legacy tool_call_id alias and transforms to a single non-optional function_call_id, so callers never need a non-null assertion; it also rejects / in ids at the boundary (the reserved state-key separator).
  • resolve.ts (replaces pending.ts)handleResolveRequest validates with safeParse and routes the decision to the per-call resume function. Returns { ok: true } or { ok: false, error: 'invalid_payload' | 'resume_failed' }.
  • redact.ts (new, extracted from denial.ts) — pure recursive redaction via Object.fromEntries, guarded by a freeze-input test. denial.ts is now just envelope construction.
  • parsePolicyReply — a discriminated-union decoder for policy::check_permissions; unknown shapes fall back to needs_approval.
  • Removedpolicy-consult.ts (folded into schemas.ts), config.ts and the approval_gate block in config.yaml (scope is fixed in code), on-decision-written.ts, register.ts, types.ts.

harness policy

harness/policy.ts (314 lines) + policy-fn.ts split into a focused harness/policy/ module:

  • handle.tsPermissionsHandle + chokidar hot-reload of iii-permissions.yaml.
  • permissions.tsPermissions.check(function_id, args) (first match wins).
  • compile.ts — rule compilation + equals / matches constraint evaluation.
  • check-permissions.tspolicy::check_permissions registration.
  • types.ts — rule / decision types.

turn-orchestrator

  • approval-resume.ts (new) — per-call turn::approval_resume::<sid>/<fcid> registration, the resume handler (persist decision → wake turn::step), and startup recovery for sessions parked across a restart.
  • hook.tsconsultBefore consults policy::check_permissions directly (5 s timeout) and maps the reply via parsePolicyReply; fails closed with a gate_unavailable denial envelope.
  • agent-call.tsdispatchWithHook returns one of result / deny / pending; pending parks the call.
  • states/functions.ts — parks pending calls into awaiting_approval, registers a resume function per call, and folds resolved decisions back into the prepared snapshot (allowpre_approved, deny / abortedblocked). config.ts drops the now-unused policy_function_id.

Wire contract preserved

Surface Behavior
handleResolveRequest { ok: true } | { ok: false; error: 'invalid_payload' | 'resume_failed' }
policy::check_permissions { decision: 'allow' | 'deny' | 'needs_approval', rule_id?, matched_constraint? }
Wire fields session_id, function_call_id, tool_call_id (fallback), rule_id, matched_constraint (snake_case)
Decision state scope approvals, key <session_id>/<function_call_id>

Also

  • runtime/state.ts: typed state::list helpers; session/tree/store.ts re-sorts entries by (timestamp, id).
  • Docs refreshed: architecture.md and workers/{approval-gate,harness,turn-orchestrator}.md.

Verification

  • tsc -b --noEmit, biome check, full vitest suite — 497 / 497 pass.
  • New suites: schemas, resolve, redact, approval-resume, plus an expanded policy.test.ts. Removed obsolete pending / types / policy-consult / on-decision-written suites.

Summary by CodeRabbit

  • Refactor

    • Approval flow redesigned so human decisions resume the exact paused call; policy/permissions logic refactored into modular components and configs simplified.
  • New Features

    • Automatic recovery on worker restart re-registers pending approvals so paused calls can be resumed transparently.
  • Bug Fixes

    • Stronger redaction for denial payloads to avoid leaking secrets and stricter policy-check validation and error handling.

Review Change Stack

Zod schemas + inferred types replace hand-rolled `typeof` validation and
`as Record<string, unknown>` casts at every boundary. Wire shape, error
codes, and `{ ok }` envelope unchanged.

- `schemas.ts` (new, replaces `types.ts`) — Zod schemas, inferred types,
  derived JSON schema for `approval::resolve`, key helpers, typed
  `state::set` and `turn::step` payloads.
- `resolve.ts` (renamed from `pending.ts`) — `ResolvePayloadSchema.transform()`
  normalises the `tool_call_id` fallback and emits a single non-optional
  `function_call_id` (no `!`). Error codes `missing_id` / `bad_decision`
  / `state_write_failed` preserved via a small classifier on the parsed
  Zod error path.
- `policy-consult.ts` (deleted) — `parsePolicyReply` and `PolicyOutcome`
  collapse into `schemas.ts` next to `PolicyReplySchema`; the discriminated
  union output is the outcome shape (no `kind`/`decision` renaming).
- `redact.ts` (new, extracted from `denial.ts`) — pure recursive
  redaction via `Object.fromEntries`; immutability guarded by test.
- `on-decision-written.ts` — `StateEventSchema` + `parsePendingKey`
  replace inline `typeof` chains and `indexOf('/')` parsing; tolerant
  key split preserves `/` inside `function_call_id` for forward
  compatibility. Silent-warn semantics preserved with an explicit
  comment.
- `register.ts` — passes `request_format: ResolvePayloadJsonSchema` on
  `approval::resolve` so the engine's directory exposes a typed signature.
- `turn-orchestrator/hook.ts` — import path migrated to `schemas.js`;
  local variable renamed `decision` → `outcome` to avoid `.decision.decision`.
- `iii-sdk` imported directly (no `runtime/iii.js` indirection).
- Tests: new `schemas.test.ts` + `redact.test.ts`, shared
  `_helpers/fakeIii.ts`, regression guards on wire error strings,
  redact immutability, and tolerant key parsing.

377 / 377 tests green. TSC strict + biome clean.
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment May 20, 2026 9:40pm

Request Review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Approval coordination now routes operator decisions via approval::resolve to per-call turn::approval_resume::<session>/<call> handlers owned by the turn-orchestrator; the policy system was split into typed compilation, permissions, and a policy::check_permissions handler; orchestrator integrates resume registration, recovery, and simplified wiring.

Changes

Approval Gate & Policy System Refactor

Layer / File(s) Summary
Policy type system and rule compilation
harness-node/src/harness/policy/types.ts, harness-node/src/harness/policy/compile.ts
Introduces permission rule types and a compilation pipeline that normalizes rule specs, compiles constraints (equals/matches), and provides constraint matching.
Permissions evaluator and file watcher
harness-node/src/harness/policy/permissions.ts, harness-node/src/harness/policy/handle.ts
Implements Permissions class to parse/compile YAML rules and evaluate calls; PermissionsHandle watches files with debounced reloads and fail-closed semantics.
policy::check_permissions remote function registration
harness-node/src/harness/policy/check-permissions.ts, harness-node/src/harness/register.ts
Registers policy::check_permissions with Zod/JSON-schema validation, maps Decision → PolicyCheckReply, and wires the permissions handle for runtime checks.
Approval-gate schemas, redaction, and denial envelopes
harness-node/src/approval-gate/schemas.ts, harness-node/src/approval-gate/redact.ts, harness-node/src/approval-gate/denial.ts
Adds Zod wire schemas for approval::resolve and policy replies, Unicode-aware clip/redact utilities, and denial envelope builders that use redact().
approval::resolve handler and worker bootstrap
harness-node/src/approval-gate/resolve.ts, harness-node/src/approval-gate/main.ts, harness-node/src/approval-gate/iii.worker.yaml
Implements approval::resolve handler validating payloads, computing per-call resume fn IDs, triggering the orchestrator resume handler, and registers it at worker bootstrap.
Turn-orchestrator direct policy checking & hook
harness-node/src/turn-orchestrator/hook.ts
consultBefore now calls policy::check_permissions and maps typed replies into HookOutcome; denial narrowed to DenialEnvelope.
Agent-call dispatch without policy_function_id threading
harness-node/src/turn-orchestrator/agent-call.ts
Removes policy_function_id from dispatch/register signatures and updates callsites; denial formatter simplified to accept DenialEnvelope.
Per-call approval resume registration and recovery
harness-node/src/turn-orchestrator/approval-resume.ts
Adds in-memory registry of per-call resume functions, resume handler that persists decision to approvals/<sid>/<cid> if absent, triggers turn::step, unregisters handlers, and recoverPendingApprovals to re-register on restart.
Abort side-effects and orchestrator config
harness-node/src/turn-orchestrator/abort.ts, harness-node/src/turn-orchestrator/config.ts
Abort now triggers per-call resume fns with {decision:'aborted'} instead of writing approvals-scoped aborted state; policy_function_id removed from orchestrator config.
Turn function execution lifecycle with approval resume
harness-node/src/turn-orchestrator/states/functions.ts
Execute registers resume hooks for pending dispatches and transitions to awaiting_approval; AwaitingApproval loads persisted decisions and patches prepared calls; Finalize persists function_results and emits lifecycle events.
Turn-orchestrator registration and recovery
harness-node/src/turn-orchestrator/register.ts
register() no longer threads policy_function_id to agent-call registration and awaits recoverPendingApprovals(iii) during startup.
Generic state::list response parsing
harness-node/src/runtime/state.ts
Adds helpers to parse multiple state::list wire shapes and stateListValues<T>; deprecates old stateList prefix behavior.
Session store state::list integration
harness-node/src/session/tree/store.ts
Replaces ad-hoc state::list unwrapping with shared parsing/type-guard helpers.
Approval-gate test coverage
harness-node/tests/approval-gate/*
Adds fakeIii test helper, redact/clip tests, resolve handler tests, schema tests, and denial-envelope integration tests.
Policy system adversarial test coverage
harness-node/tests/harness/policy.test.ts
Extensive tests for malformed policies, type confusion, prototype-pollution guards, constraint matching (including regex footguns), decision mapping, handler validation, and PermissionsHandle fail-closed behavior.
Turn-orchestrator integration tests
harness-node/tests/turn-orchestrator/*, harness-node/tests/integration/approval-resume.e2e.test.ts
Updates to test harness and tests to validate approval-resume registration/recovery, consultBefore typed replies, dispatch changes, abort behavior, and e2e approval-resume flow.
State::list parsing tests
harness-node/tests/runtime/state-list.test.ts
Tests for parsing flat arrays, {value} envelopes, {items} envelopes, and keyed entries.
Architecture and worker documentation
harness-node/README.md, harness-node/docs/architecture.md, harness-node/docs/workers/{approval-gate,harness,turn-orchestrator}.md
Docs updated to reflect the new per-call approval-resume flow, modularized policy files, removal of approval-gate state-trigger/config, and kernel deny-list shorthand.
Configuration cleanup
harness-node/config.yaml, iii-permissions.yaml
Removed policy_function_id and approval_gate config; iii-permissions.yaml kernel deny list rewritten to !function_id shorthand.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • iii-hq/workers#159: Overlapping approval-path refactor and removal of legacy approval-gate wiring.
  • iii-hq/workers#156: Related work on reactive approval resume and abort behavior changes.

Suggested reviewers

  • sergiofilhowz
  • andersonleal

🐰 "I hopped through schemas, redaction, and queue—
Per-call resumes now wake the turn anew.
From state triggers past, to handlers so neat,
A rabbit applauds this tidy approval feat!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/approval-gate-typescript-idioms

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 11 skipped (no docs/).

Layer Result
structure
vale
ai

Three for three. Nicely done.

…ecated configurations

This commit refactors the approval-gate to enhance the handling of approval states and decisions. Key changes include:

- Removed the `approval_gate` section from `config.yaml` as it is no longer needed.
- Simplified the approval process by directly routing decisions to per-call `turn::approval_resume` functions in the turn-orchestrator.
- Eliminated the `approval_gate.approval_state_scope` configuration, fixing the scope to `approvals` in code.
- Updated documentation to reflect the new approval flow and removed outdated references to state triggers and adapters that are no longer in use.

These changes improve clarity and maintainability of the approval-gate functionality, ensuring a more efficient approval process.
…uration

This commit refactors the policy handling within the harness-node to improve clarity and maintainability. Key changes include:

- Removed the deprecated `policy_function_id` from `config.yaml` and related documentation, as it is now directly handled in the orchestrator.
- Introduced a new `check-permissions.ts` file that encapsulates the logic for checking permissions, replacing the previous `policy-fn.ts`.
- Updated the `dispatchWithHook` function to directly call the new permissions check, simplifying the approval process.
- Enhanced the redaction logic in `redact.ts` to prevent stack overflow from deeply nested structures.
- Added validation to ensure that session and function IDs do not contain reserved characters.

These changes streamline the policy evaluation process and improve the overall structure of the codebase, ensuring a more efficient and robust implementation.
ytallo added 2 commits May 20, 2026 12:00
…-gate note

Replace the deleted src/harness/policy.ts source-layout row with the new
src/harness/policy/ module files, and remove the dead note describing the
removed approval::on_decision_written adapter.
Resolved conflicts in turn-orchestrator states/functions.ts and the
turn-orchestrator doc by combining both designs: keep this branch's
approval refactor (STATE_SCOPE, registerApprovalResume, 3-arg
dispatchWithHook, no policy_function_id) and main's duration_ms
function-execution timing. Reordered store.ts imports to satisfy biome.
@ytallo
ytallo marked this pull request as ready for review May 20, 2026 15:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
harness-node/src/harness/policy/compile.ts (1)

57-60: ⚡ Quick win

Consider adding regex safety validation to defensive-code against pathological patterns in policy config.

The code at line 59 compiles regex patterns from iii-permissions.yaml without checking for catastrophic backtracking. While the file is a trusted, git-tracked system configuration with reasonable patterns (e.g., ^session/[a-z]+/notes$, ^git (status|log|diff)( |$)), an operator with commit access could introduce a pathological regex that causes CPU DoS during policy checks. The comment mentions "Rust regex crate" but the code uses JavaScript RegExp, which lacks automatic backtracking prevention. Adding a safety check using safe-regex2 at load time (when compileConstraint runs) would be defensive hardening without performance cost since compilation happens once at boot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/harness/policy/compile.ts` around lines 57 - 60,
compileConstraint currently constructs a RegExp directly from c.matches; add a
safety check to reject pathological patterns by validating c.matches with a
safe-regex checker (e.g., safe-regex2) before creating the RegExp. Import and
call the safe-regex validator inside the branch that handles 'matches' (where
c.matches is used), and if the validator returns unsafe, throw or return a clear
compile-time error instead of compiling; only call new RegExp(c.matches) when
the pattern is reported safe. Ensure the thrown error is handled consistently
with the existing try/catch around compileConstraint so boot-time loading fails
fast for unsafe patterns.
harness-node/tests/harness/policy.test.ts (1)

395-399: ⚡ Quick win

Resolve the shipped policy fixture relative to the test module.

readFile('./iii-permissions.yaml', 'utf8') depends on the process CWD, which is ambiguous when vitest runs without an explicit root configuration. While a symlink at harness-node/iii-permissions.yaml currently masks this issue, using import.meta.url is the robust pattern for ESM.

♻️ Proposed fix
   const load = async () => {
     if (!shipped) {
       const { readFile } = await import('node:fs/promises');
-      shipped = Permissions.parse(await readFile('./iii-permissions.yaml', 'utf8'));
+      const policyFile = new URL('../../iii-permissions.yaml', import.meta.url);
+      shipped = Permissions.parse(await readFile(policyFile, 'utf8'));
     }
     return shipped;
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/tests/harness/policy.test.ts` around lines 395 - 399, The load
function uses readFile('./iii-permissions.yaml', 'utf8') which relies on process
CWD; change it to resolve the fixture relative to the test module by using
import.meta.url. Specifically, update the call inside load so
Permissions.parse(await readFile(...)) reads from new
URL('./iii-permissions.yaml', import.meta.url) (i.e., Permissions.parse(await
readFile(new URL('./iii-permissions.yaml', import.meta.url), 'utf8'))), leaving
shipped and Permissions.parse unchanged.
harness-node/tests/turn-orchestrator/approval-resume.test.ts (1)

26-34: ⚡ Quick win

Make unregister actually detach handlers in this test double.

Right now unregister is a no-op spy, so function IDs remain callable after “unregister,” which can mask lifecycle bugs around resume-handler cleanup.

Proposed patch
   const iii = {
     registerFunction: vi.fn((fnId: string, handler: (payload: unknown) => Promise<unknown>) => {
-      const entry: RegisteredFn = {
+      const unregister = vi.fn(() => {
+        registered.delete(fnId);
+      });
+      const entry: RegisteredFn = {
         fnId,
         handler,
-        unregister: vi.fn(),
+        unregister,
       };
       registered.set(fnId, entry);
       return { unregister: entry.unregister };
     }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/tests/turn-orchestrator/approval-resume.test.ts` around lines 26
- 34, The test double's registerFunction currently returns a no-op spy as
unregister so entries stay in the registered map after "unregister"; change the
implementation so unregister actually removes the handler from the registered
Map: when creating the RegisteredFn entry in registerFunction, set unregister to
a function (wrapped with vi.fn if you need call-tracking) that calls
registered.delete(fnId) (and is idempotent), and return that unregister; update
any references to RegisteredFn.unregister to use this removal behavior so
handlers cannot be invoked after unregister.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@harness-node/src/runtime/state.ts`:
- Around line 132-139: stateList currently ignores the optional _prefix and
returns the whole scope; preserve the deprecated wrapper contract by calling
stateListValues(iii, { scope }) and then, when _prefix is provided, locally
filter the returned array to only entries whose key begins with that prefix
(e.g., entry.key.startsWith(_prefix) or adapt to the actual entry shape), then
return the filtered list; update stateList (and mention stateListValues) to
perform this conditional filtering so callers get prefix-scoped results until
they are migrated.
- Around line 39-73: The parsers currently treat any object with a "value"
property as a wrapper and unwrap it, corrupting legitimate stored objects like {
value: 1, state: 'x' }; update unwrapStateListEntry, parseStateListValues and
parseStateListKeyedEntries (and use stateListResponseRows as before) to only
unwrap when the object shape is unambiguously an envelope: for the simple value
envelope require the object’s own enumerable keys are exactly ["value"] (or
exactly ["key","value"] for keyed envelope cases), and for the items-envelope
accept only objects whose own keys are exactly ["items"] and whose items are an
array of { key, value } shapes; otherwise return the original object as the
stored value. Ensure these key checks use Object.keys(...) so other properties
prevent unwrapping.

In `@harness-node/src/turn-orchestrator/approval-resume.ts`:
- Around line 83-90: The current read-then-write using pendingKey(session_id,
function_call_id) with stateGet(iii, STATE_SCOPE, key) and stateSet(...) is
racy: multiple callers can observe no decision and both write; change to a
first-writer-wins atomic update or an in-memory per-function-call guard.
Concretely, replace the separated stateGet/stateSet sequence (and the
hasStoredDecision check) with either a conditional/compare-and-set style
operation provided by the state layer (an atomic "set-if-missing" or
"compareAndSwap" using key) so only the first writer persists
parsed.data.decision and parsed.data.reason, or add a short-lived in-process
guard keyed by pendingKey(session_id, function_call_id) (acquire guard before
awaiting stateGet and release after stateSet) to ensure only one in-flight
resolver can perform the write. Ensure the final write only occurs when the
atomic operation succeeds or when the guard owns the key.
- Around line 92-99: The current catch block unregisters the resume handler even
when iii.trigger({ function_id: STEP_FN_ID, payload: { session_id } }) fails,
removing the only retry path; change the flow so unregisterApprovalResume(fnId)
is only called after a successful wake (i.e., move or call
unregisterApprovalResume(fnId) inside the try block immediately after the await)
and do not call it inside the catch so the handler remains registered for
retries; ensure fnId and STEP_FN_ID references remain unchanged.

In `@harness-node/src/turn-orchestrator/hook.ts`:
- Around line 42-65: The switch on reply.decision can fall through for malformed
or unexpected PolicyCheckReply values and return undefined; modify the switch in
consultBefore (the block after iii.trigger<CheckPermissionsPayload,
PolicyCheckReply>) to handle unknown/missing replies by treating them as a
closed gate: add a default/fallback branch that returns a deny outcome with a
denial envelope indicating a gate_unavailable error (use
permissionsDenyEnvelope(function_call.function_id, 'gate_unavailable', null,
function_call.arguments) or equivalent), ensuring you still use reply.rule_id
and reply.matched_constraint when present but fall back to safe defaults when
they are absent.

---

Nitpick comments:
In `@harness-node/src/harness/policy/compile.ts`:
- Around line 57-60: compileConstraint currently constructs a RegExp directly
from c.matches; add a safety check to reject pathological patterns by validating
c.matches with a safe-regex checker (e.g., safe-regex2) before creating the
RegExp. Import and call the safe-regex validator inside the branch that handles
'matches' (where c.matches is used), and if the validator returns unsafe, throw
or return a clear compile-time error instead of compiling; only call new
RegExp(c.matches) when the pattern is reported safe. Ensure the thrown error is
handled consistently with the existing try/catch around compileConstraint so
boot-time loading fails fast for unsafe patterns.

In `@harness-node/tests/harness/policy.test.ts`:
- Around line 395-399: The load function uses readFile('./iii-permissions.yaml',
'utf8') which relies on process CWD; change it to resolve the fixture relative
to the test module by using import.meta.url. Specifically, update the call
inside load so Permissions.parse(await readFile(...)) reads from new
URL('./iii-permissions.yaml', import.meta.url) (i.e., Permissions.parse(await
readFile(new URL('./iii-permissions.yaml', import.meta.url), 'utf8'))), leaving
shipped and Permissions.parse unchanged.

In `@harness-node/tests/turn-orchestrator/approval-resume.test.ts`:
- Around line 26-34: The test double's registerFunction currently returns a
no-op spy as unregister so entries stay in the registered map after
"unregister"; change the implementation so unregister actually removes the
handler from the registered Map: when creating the RegisteredFn entry in
registerFunction, set unregister to a function (wrapped with vi.fn if you need
call-tracking) that calls registered.delete(fnId) (and is idempotent), and
return that unregister; update any references to RegisteredFn.unregister to use
this removal behavior so handlers cannot be invoked after unregister.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c4a1657-f049-461a-bc32-f48eb8040e61

📥 Commits

Reviewing files that changed from the base of the PR and between 610c0e0 and 259a9b7.

📒 Files selected for processing (55)
  • harness-node/README.md
  • harness-node/config.yaml
  • harness-node/docs/architecture.md
  • harness-node/docs/workers/approval-gate.md
  • harness-node/docs/workers/harness.md
  • harness-node/docs/workers/turn-orchestrator.md
  • harness-node/src/approval-gate/config.ts
  • harness-node/src/approval-gate/denial.ts
  • harness-node/src/approval-gate/iii.worker.yaml
  • harness-node/src/approval-gate/main.ts
  • harness-node/src/approval-gate/on-decision-written.ts
  • harness-node/src/approval-gate/pending.ts
  • harness-node/src/approval-gate/policy-consult.ts
  • harness-node/src/approval-gate/redact.ts
  • harness-node/src/approval-gate/register.ts
  • harness-node/src/approval-gate/resolve.ts
  • harness-node/src/approval-gate/schemas.ts
  • harness-node/src/approval-gate/types.ts
  • harness-node/src/harness/policy-fn.ts
  • harness-node/src/harness/policy.ts
  • harness-node/src/harness/policy/check-permissions.ts
  • harness-node/src/harness/policy/compile.ts
  • harness-node/src/harness/policy/handle.ts
  • harness-node/src/harness/policy/permissions.ts
  • harness-node/src/harness/policy/types.ts
  • harness-node/src/harness/register.ts
  • harness-node/src/index.ts
  • harness-node/src/runtime/state.ts
  • harness-node/src/session/tree/store.ts
  • harness-node/src/turn-orchestrator/abort.ts
  • harness-node/src/turn-orchestrator/agent-call.ts
  • harness-node/src/turn-orchestrator/approval-resume.ts
  • harness-node/src/turn-orchestrator/config.ts
  • harness-node/src/turn-orchestrator/hook.ts
  • harness-node/src/turn-orchestrator/on-terminal.ts
  • harness-node/src/turn-orchestrator/register.ts
  • harness-node/src/turn-orchestrator/states/functions.ts
  • harness-node/tests/approval-gate/_helpers/fakeIii.ts
  • harness-node/tests/approval-gate/denial.test.ts
  • harness-node/tests/approval-gate/on-decision-written.test.ts
  • harness-node/tests/approval-gate/pending.test.ts
  • harness-node/tests/approval-gate/policy-consult.test.ts
  • harness-node/tests/approval-gate/redact.test.ts
  • harness-node/tests/approval-gate/resolve.test.ts
  • harness-node/tests/approval-gate/schemas.test.ts
  • harness-node/tests/approval-gate/types.test.ts
  • harness-node/tests/harness/policy.test.ts
  • harness-node/tests/integration/approval-resume.e2e.test.ts
  • harness-node/tests/runtime/state-list.test.ts
  • harness-node/tests/turn-orchestrator/abort.test.ts
  • harness-node/tests/turn-orchestrator/agent-call.test.ts
  • harness-node/tests/turn-orchestrator/approval-resume.test.ts
  • harness-node/tests/turn-orchestrator/config.test.ts
  • harness-node/tests/turn-orchestrator/functions.test.ts
  • harness-node/tests/turn-orchestrator/hook.test.ts
💤 Files with no reviewable changes (14)
  • harness-node/tests/approval-gate/policy-consult.test.ts
  • harness-node/src/harness/policy-fn.ts
  • harness-node/src/approval-gate/register.ts
  • harness-node/tests/approval-gate/types.test.ts
  • harness-node/src/approval-gate/types.ts
  • harness-node/tests/approval-gate/on-decision-written.test.ts
  • harness-node/src/approval-gate/on-decision-written.ts
  • harness-node/src/approval-gate/policy-consult.ts
  • harness-node/tests/approval-gate/pending.test.ts
  • harness-node/src/approval-gate/config.ts
  • harness-node/src/approval-gate/pending.ts
  • harness-node/config.yaml
  • harness-node/src/harness/policy.ts
  • harness-node/tests/turn-orchestrator/agent-call.test.ts

Comment on lines +39 to +73
function unwrapStateListEntry<T>(entry: unknown): T {
if (entry && typeof entry === 'object' && 'value' in (entry as Record<string, unknown>)) {
return (entry as Record<string, unknown>).value as T;
}
return entry as T;
}

/**
* Normalizes a `state::list` trigger result to stored values.
*
* Official iii returns a flat `T[]` ({@link StateListInput} only). Some
* deployments also wrap rows as `{ value }` or `{ items: [{ key, value }] }`;
* we accept those shapes so harness workers stay compatible.
*/
export function parseStateListValues<T>(response: unknown): T[] {
const arr = stateListResponseRows(response);
if (!arr) return [];
return arr.map((entry) => unwrapStateListEntry<T>(entry));
}

/** Keyed rows when the list response includes `key` (not returned by stock iii). */
export function parseStateListKeyedEntries(response: unknown): StateListKeyedEntry[] {
const arr = stateListResponseRows(response);
if (!arr) return [];
return arr.map((entry) => {
if (entry && typeof entry === 'object') {
const row = entry as Record<string, unknown>;
return {
key: typeof row.key === 'string' ? row.key : undefined,
value: row.value !== undefined ? row.value : entry,
};
}
return { value: entry };
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disambiguate list envelopes from stored objects.

Both parsers treat any object with a value field as a wrapper row. For the official flat T[] shape, a legitimate stored value like { value: 1, state: 'x' } gets collapsed to 1, which corrupts list results. Only unwrap when the row shape is unambiguously an envelope.

Proposed fix
 function unwrapStateListEntry<T>(entry: unknown): T {
-  if (entry && typeof entry === 'object' && 'value' in (entry as Record<string, unknown>)) {
-    return (entry as Record<string, unknown>).value as T;
+  if (entry && typeof entry === 'object') {
+    const row = entry as Record<string, unknown>;
+    const keys = Object.keys(row);
+    if ('value' in row && keys.every((k) => k === 'value' || k === 'key')) {
+      return row.value as T;
+    }
   }
   return entry as T;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function unwrapStateListEntry<T>(entry: unknown): T {
if (entry && typeof entry === 'object' && 'value' in (entry as Record<string, unknown>)) {
return (entry as Record<string, unknown>).value as T;
}
return entry as T;
}
/**
* Normalizes a `state::list` trigger result to stored values.
*
* Official iii returns a flat `T[]` ({@link StateListInput} only). Some
* deployments also wrap rows as `{ value }` or `{ items: [{ key, value }] }`;
* we accept those shapes so harness workers stay compatible.
*/
export function parseStateListValues<T>(response: unknown): T[] {
const arr = stateListResponseRows(response);
if (!arr) return [];
return arr.map((entry) => unwrapStateListEntry<T>(entry));
}
/** Keyed rows when the list response includes `key` (not returned by stock iii). */
export function parseStateListKeyedEntries(response: unknown): StateListKeyedEntry[] {
const arr = stateListResponseRows(response);
if (!arr) return [];
return arr.map((entry) => {
if (entry && typeof entry === 'object') {
const row = entry as Record<string, unknown>;
return {
key: typeof row.key === 'string' ? row.key : undefined,
value: row.value !== undefined ? row.value : entry,
};
}
return { value: entry };
});
}
function unwrapStateListEntry<T>(entry: unknown): T {
if (entry && typeof entry === 'object') {
const row = entry as Record<string, unknown>;
const keys = Object.keys(row);
if ('value' in row && keys.every((k) => k === 'value' || k === 'key')) {
return row.value as T;
}
}
return entry as T;
}
/**
* Normalizes a `state::list` trigger result to stored values.
*
* Official iii returns a flat `T[]` ({`@link` StateListInput} only). Some
* deployments also wrap rows as `{ value }` or `{ items: [{ key, value }] }`;
* we accept those shapes so harness workers stay compatible.
*/
export function parseStateListValues<T>(response: unknown): T[] {
const arr = stateListResponseRows(response);
if (!arr) return [];
return arr.map((entry) => unwrapStateListEntry<T>(entry));
}
/** Keyed rows when the list response includes `key` (not returned by stock iii). */
export function parseStateListKeyedEntries(response: unknown): StateListKeyedEntry[] {
const arr = stateListResponseRows(response);
if (!arr) return [];
return arr.map((entry) => {
if (entry && typeof entry === 'object') {
const row = entry as Record<string, unknown>;
return {
key: typeof row.key === 'string' ? row.key : undefined,
value: row.value !== undefined ? row.value : entry,
};
}
return { value: entry };
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/runtime/state.ts` around lines 39 - 73, The parsers
currently treat any object with a "value" property as a wrapper and unwrap it,
corrupting legitimate stored objects like { value: 1, state: 'x' }; update
unwrapStateListEntry, parseStateListValues and parseStateListKeyedEntries (and
use stateListResponseRows as before) to only unwrap when the object shape is
unambiguously an envelope: for the simple value envelope require the object’s
own enumerable keys are exactly ["value"] (or exactly ["key","value"] for keyed
envelope cases), and for the items-envelope accept only objects whose own keys
are exactly ["items"] and whose items are an array of { key, value } shapes;
otherwise return the original object as the stored value. Ensure these key
checks use Object.keys(...) so other properties prevent unwrapping.

Comment on lines +132 to +139
/**
* @deprecated Third argument `prefix` is not sent to iii (engine lists the
* whole scope). Kept for call-site stability; filter returned values locally
* if you need key-prefix semantics.
*/
export async function stateList(iii: ISdk, scope: string, _prefix?: string): Promise<unknown[]> {
return stateListValues(iii, { scope });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep the deprecated wrapper's prefix contract until callers are migrated.

stateList(iii, scope, prefix) still accepts prefix, but this implementation silently widens to the whole scope. That makes unchanged callers return unrelated entries. Either filter locally when keys are available, or remove the parameter/export in a breaking change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/runtime/state.ts` around lines 132 - 139, stateList
currently ignores the optional _prefix and returns the whole scope; preserve the
deprecated wrapper contract by calling stateListValues(iii, { scope }) and then,
when _prefix is provided, locally filter the returned array to only entries
whose key begins with that prefix (e.g., entry.key.startsWith(_prefix) or adapt
to the actual entry shape), then return the filtered list; update stateList (and
mention stateListValues) to perform this conditional filtering so callers get
prefix-scoped results until they are migrated.

Comment on lines +83 to +90
const key = pendingKey(session_id, function_call_id);
const existing = await stateGet(iii, STATE_SCOPE, key);
if (!hasStoredDecision(existing)) {
await stateSet(iii, STATE_SCOPE, key, {
decision: parsed.data.decision,
reason: parsed.data.reason,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make approval resolution first-writer-wins.

stateGet() and stateSet() are separated by awaits, so an abort and a user approval can both observe “no decision” and then overwrite each other. This path needs an atomic write or a per-fnId in-flight guard before the first await.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/turn-orchestrator/approval-resume.ts` around lines 83 - 90,
The current read-then-write using pendingKey(session_id, function_call_id) with
stateGet(iii, STATE_SCOPE, key) and stateSet(...) is racy: multiple callers can
observe no decision and both write; change to a first-writer-wins atomic update
or an in-memory per-function-call guard. Concretely, replace the separated
stateGet/stateSet sequence (and the hasStoredDecision check) with either a
conditional/compare-and-set style operation provided by the state layer (an
atomic "set-if-missing" or "compareAndSwap" using key) so only the first writer
persists parsed.data.decision and parsed.data.reason, or add a short-lived
in-process guard keyed by pendingKey(session_id, function_call_id) (acquire
guard before awaiting stateGet and release after stateSet) to ensure only one
in-flight resolver can perform the write. Ensure the final write only occurs
when the atomic operation succeeds or when the guard owns the key.

Comment on lines +92 to +99
try {
await iii.trigger({ function_id: STEP_FN_ID, payload: { session_id } });
} catch (err) {
logger.warn('approval resume: turn::step invoke failed', { session_id, err: String(err) });
}

unregisterApprovalResume(fnId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't unregister the resume function after a failed wake-up.

If turn::step throws, Line 98 still removes the only retry path. The decision is already persisted, so the session can stay parked until restart/manual intervention. Keep the handler registered on failure so the wake can be retried.

Proposed fix
   try {
     await iii.trigger({ function_id: STEP_FN_ID, payload: { session_id } });
+    unregisterApprovalResume(fnId);
   } catch (err) {
     logger.warn('approval resume: turn::step invoke failed', { session_id, err: String(err) });
   }
-
-  unregisterApprovalResume(fnId);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await iii.trigger({ function_id: STEP_FN_ID, payload: { session_id } });
} catch (err) {
logger.warn('approval resume: turn::step invoke failed', { session_id, err: String(err) });
}
unregisterApprovalResume(fnId);
}
try {
await iii.trigger({ function_id: STEP_FN_ID, payload: { session_id } });
unregisterApprovalResume(fnId);
} catch (err) {
logger.warn('approval resume: turn::step invoke failed', { session_id, err: String(err) });
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/turn-orchestrator/approval-resume.ts` around lines 92 - 99,
The current catch block unregisters the resume handler even when iii.trigger({
function_id: STEP_FN_ID, payload: { session_id } }) fails, removing the only
retry path; change the flow so unregisterApprovalResume(fnId) is only called
after a successful wake (i.e., move or call unregisterApprovalResume(fnId)
inside the try block immediately after the await) and do not call it inside the
catch so the handler remains registered for retries; ensure fnId and STEP_FN_ID
references remain unchanged.

Comment on lines +42 to +65
const reply = await iii.trigger<CheckPermissionsPayload, PolicyCheckReply>({
function_id: 'policy::check_permissions',
payload: {
function_id: function_call.function_id,
args: function_call.arguments as CheckPermissionsPayload['args'],
},
timeoutMs: 5_000,
});
switch (reply.decision) {
case 'allow':
return { kind: 'allow' };
case 'deny':
return {
kind: 'deny',
denial: permissionsDenyEnvelope(
function_call.function_id,
reply.rule_id,
reply.matched_constraint ?? null,
function_call.arguments,
),
};
case 'needs_approval':
return { kind: 'pending' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed on malformed policy replies.

If policy::check_permissions returns an unexpected payload, this switch falls through and consultBefore() resolves undefined. The next access to outcome.kind then throws instead of denying. Treat an unknown reply shape/value as gate_unavailable here.

Proposed fix
-    switch (reply.decision) {
+    const decision =
+      reply && typeof reply === 'object'
+        ? (reply as Record<string, unknown>).decision
+        : undefined;
+    switch (decision) {
       case 'allow':
         return { kind: 'allow' };
       case 'deny':
         return {
           kind: 'deny',
@@
       case 'needs_approval':
         return { kind: 'pending' };
+      default:
+        throw new Error('malformed policy reply');
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const reply = await iii.trigger<CheckPermissionsPayload, PolicyCheckReply>({
function_id: 'policy::check_permissions',
payload: {
function_id: function_call.function_id,
args: function_call.arguments as CheckPermissionsPayload['args'],
},
timeoutMs: 5_000,
});
switch (reply.decision) {
case 'allow':
return { kind: 'allow' };
case 'deny':
return {
kind: 'deny',
denial: permissionsDenyEnvelope(
function_call.function_id,
reply.rule_id,
reply.matched_constraint ?? null,
function_call.arguments,
),
};
case 'needs_approval':
return { kind: 'pending' };
}
const reply = await iii.trigger<CheckPermissionsPayload, PolicyCheckReply>({
function_id: 'policy::check_permissions',
payload: {
function_id: function_call.function_id,
args: function_call.arguments as CheckPermissionsPayload['args'],
},
timeoutMs: 5_000,
});
const decision =
reply && typeof reply === 'object'
? (reply as Record<string, unknown>).decision
: undefined;
switch (decision) {
case 'allow':
return { kind: 'allow' };
case 'deny':
return {
kind: 'deny',
denial: permissionsDenyEnvelope(
function_call.function_id,
reply.rule_id,
reply.matched_constraint ?? null,
function_call.arguments,
),
};
case 'needs_approval':
return { kind: 'pending' };
default:
throw new Error('malformed policy reply');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/turn-orchestrator/hook.ts` around lines 42 - 65, The switch
on reply.decision can fall through for malformed or unexpected PolicyCheckReply
values and return undefined; modify the switch in consultBefore (the block after
iii.trigger<CheckPermissionsPayload, PolicyCheckReply>) to handle
unknown/missing replies by treating them as a closed gate: add a
default/fallback branch that returns a deny outcome with a denial envelope
indicating a gate_unavailable error (use
permissionsDenyEnvelope(function_call.function_id, 'gate_unavailable', null,
function_call.arguments) or equivalent), ensuring you still use reply.rule_id
and reply.matched_constraint when present but fall back to safe defaults when
they are absent.

…ance clarity

This commit refactors the iii-permissions.yaml file to streamline the permissions rules for agents. Key changes include:

- Consolidated the permissions rules by removing verbose comments and restructuring the format for clarity.
- Replaced individual deny rules with a simplified notation using bare strings for allow and quoted strings for deny.
- Removed deprecated sections and comments, focusing on essential permissions and their intended use.

These changes improve the readability and maintainability of the permissions configuration, ensuring a more efficient setup for agent permissions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@iii-permissions.yaml`:
- Around line 10-27: The deny list currently blocks '!approval::resolve' but not
the new resume handler, so agents can discover and call the internal resume via
'directory::engine::functions::list' and fallback to 'needs_approval'; add a
deny rule for the 'turn::approval_resume' namespace (e.g., add a line like the
other entries: '- '!turn::approval_resume'') to the deny block in
iii-permissions.yaml so that any calls under the turn::approval_resume prefix
are prevented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24e69b1a-1a6d-4cda-9c74-301c384749f9

📥 Commits

Reviewing files that changed from the base of the PR and between 259a9b7 and b383f8e.

📒 Files selected for processing (1)
  • iii-permissions.yaml

Comment thread iii-permissions.yaml
Comment on lines +10 to +27
# Gate, state, auth, routing — agents must not call these directly.
- '!approval::resolve'
- '!policy::check_permissions'
- '!hook-fanout::publish_collect'
- '!state::set'
- '!state::update'
- '!state::delete'
- '!stream::set'
- '!iii::durable::publish'
- '!auth::set_token'
- '!auth::delete_token'
- '!oauth::anthropic::login'
- '!oauth::openai-codex::login'
- '!run::start'
- '!run::start_and_wait'
- '!router::stream_assistant'
- '!router::abort'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Deny the new turn::approval_resume control-plane functions.

This refactor moved approval completion behind per-call resume functions, but the kernel deny list here still only blocks approval::resolve. Because directory::engine::functions::list is allowed and unmatched calls fall back to needs_approval, agents can discover and request approval on the internal resume handler instead of the original tool call. Add a deny rule that matches the turn::approval_resume namespace/prefix as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@iii-permissions.yaml` around lines 10 - 27, The deny list currently blocks
'!approval::resolve' but not the new resume handler, so agents can discover and
call the internal resume via 'directory::engine::functions::list' and fallback
to 'needs_approval'; add a deny rule for the 'turn::approval_resume' namespace
(e.g., add a line like the other entries: '- '!turn::approval_resume'') to the
deny block in iii-permissions.yaml so that any calls under the
turn::approval_resume prefix are prevented.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
harness-node/src/runtime/state.ts (2)

142-143: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The deprecated stateList wrapper still drops prefix semantics.

_prefix is accepted but ignored, so unchanged callers can now receive the whole scope instead of prefix-scoped results. Preserve behavior until migration (pass prefix through or conditionally filter when keyed rows are available).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/runtime/state.ts` around lines 142 - 143, The stateList
wrapper currently accepts _prefix but ignores it, causing callers to lose
prefix-scoped behavior; update the stateList function to forward the _prefix to
stateListValues (e.g., stateListValues(iii, { scope, prefix: _prefix })) or, if
stateListValues returns keyed rows only in some cases, apply conditional
filtering inside stateList using the _prefix to preserve the original
prefix-scoped results; refer to the stateList function signature and the
stateListValues call to implement the fix.

35-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only unwrap list envelopes when the row shape is unambiguous.

The current parsing still unwraps any object with a value property, which can corrupt legitimate stored objects (e.g. { value: 1, state: 'x' } becomes 1). Restrict unwrapping to envelope-only shapes ({value} or {key,value}), and keep all other objects intact.

Proposed minimal fix
 function unwrapStateListEntry<T>(entry: unknown): T {
-  if (entry && typeof entry === 'object' && 'value' in (entry as Record<string, unknown>)) {
-    return (entry as Record<string, unknown>).value as T;
+  if (entry && typeof entry === 'object') {
+    const row = entry as Record<string, unknown>;
+    const keys = Object.keys(row);
+    if ('value' in row && keys.every((k) => k === 'value' || k === 'key')) {
+      return row.value as T;
+    }
   }
   return entry as T;
 }

Also applies to: 44-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness-node/src/runtime/state.ts` around lines 35 - 39, The code currently
unwraps any object with a value property which can corrupt real objects; update
stateListResponseRows so it only unwraps when the object shape is an
envelope-only form—i.e., when the item is an object whose own property names are
exactly ["value"] (unwrap to item.value) or exactly ["key","value"] (unwrap to {
key, value }.value? actually keep semantics: return the item.value for
value-only envelopes and return items mapped to { key, value } for key/value
envelopes as before) — otherwise return the original object intact; apply the
same exact envelope-only check and behavior to the other envelope-unwrapping
location in this file (the nearby single-item/single-response unwrap block) so
only unambiguous {value} or {key,value} shapes are unwrapped.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@harness-node/src/runtime/state.ts`:
- Around line 142-143: The stateList wrapper currently accepts _prefix but
ignores it, causing callers to lose prefix-scoped behavior; update the stateList
function to forward the _prefix to stateListValues (e.g., stateListValues(iii, {
scope, prefix: _prefix })) or, if stateListValues returns keyed rows only in
some cases, apply conditional filtering inside stateList using the _prefix to
preserve the original prefix-scoped results; refer to the stateList function
signature and the stateListValues call to implement the fix.
- Around line 35-39: The code currently unwraps any object with a value property
which can corrupt real objects; update stateListResponseRows so it only unwraps
when the object shape is an envelope-only form—i.e., when the item is an object
whose own property names are exactly ["value"] (unwrap to item.value) or exactly
["key","value"] (unwrap to { key, value }.value? actually keep semantics: return
the item.value for value-only envelopes and return items mapped to { key, value
} for key/value envelopes as before) — otherwise return the original object
intact; apply the same exact envelope-only check and behavior to the other
envelope-unwrapping location in this file (the nearby
single-item/single-response unwrap block) so only unambiguous {value} or
{key,value} shapes are unwrapped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 658ecffb-3f99-4655-bf9c-ceca1f90250d

📥 Commits

Reviewing files that changed from the base of the PR and between b383f8e and 9246ee1.

📒 Files selected for processing (1)
  • harness-node/src/runtime/state.ts

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.

2 participants