Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f98627
docs(plans): freshclaude parity and alerts audit plans
danshapiro Oct 22, 2026
76dde0f
test(claude): real-CLI contract suite pins the CLI facts
danshapiro Oct 22, 2026
17835f9
test(claude): CLI simulator that matches the real CLI
danshapiro Oct 22, 2026
f1015be
test(claude): the CLI simulator writes nothing after its stream ends
danshapiro Oct 22, 2026
be7a0a8
test(claude): the CLI simulator refuses an unknown permission mode li…
danshapiro Oct 22, 2026
a0a5634
refactor(claude-sidecar): checkpoint identity binding is a pure module
danshapiro Oct 22, 2026
ef0527f
test(claude-sidecar): unit tests run the real index.mjs over the simu…
danshapiro Oct 22, 2026
254df0c
test(claude-sidecar): the real-sidecar interrupt test stages every si…
danshapiro Oct 22, 2026
9c71b1d
test(rust): RealSidecarSim and RealSidecarCli rigs
danshapiro Oct 22, 2026
827d939
fix(freshclaude): a new pane's checkpointed sends are not refused
danshapiro Oct 22, 2026
35af24a
fix(freshclaude): a user Stop shows no error banner (G03)
danshapiro Oct 22, 2026
903f62c
fix(freshclaude): the Stop gate silences only aborted results (G06 bell)
danshapiro Oct 22, 2026
c1351fc
feat(freshclaude): structured sidecar logs and edge logs (G14)
danshapiro Oct 22, 2026
4f8b5f9
fix(freshclaude): a boot refusal is never the user's Stop (PRA-CORR-01)
danshapiro Oct 22, 2026
1ca51fa
refactor(freshclaude): attention edges leave only through emit_attent…
danshapiro Oct 22, 2026
6c08b4d
test(claude-sidecar): settings tests pin the refused mode and the man…
danshapiro Oct 22, 2026
bdfe265
fix(freshclaude): a send's owner fence is checked before the session …
danshapiro Oct 22, 2026
566e1f5
test(claude): the simulator refuses an approval step under bypassPerm…
danshapiro Oct 22, 2026
9e05a5f
refactor(claude-sidecar): one hosted-mode module with named rules (F6)
danshapiro Oct 22, 2026
6fd5932
docs(plans): hand the hosted-kilroy runtime decision to api-hosted-op…
danshapiro Oct 22, 2026
53d482d
feat(freshclaude): the model catalog probe's log records keep their s…
danshapiro Oct 22, 2026
a042f7d
test(claude): each real-CLI scenario runs once per test run (F8)
danshapiro Oct 22, 2026
80cee68
Merge origin/main into feat/freshclaude-parity
danshapiro Oct 23, 2026
e128d37
test(claude): the fork scenario closes its sessions only after the tu…
danshapiro Oct 23, 2026
a36b9a4
test(claude-sidecar): pin the stdout.closed warn record when the stdo…
danshapiro Oct 23, 2026
1691c4e
test(claude): main's shutdown and capture harnesses use the shared pi…
danshapiro Oct 23, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions crates/freshell-claude-sidecar/checkpoint-identity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Checkpoint identity at the SDK replay boundary (pure; used by index.mjs).
//
// A checkpointed send carries a v4 uuid on its SDKUserMessage. The CLI echoes
// a message it takes back as a user row with `isReplay: true` and that uuid
// (the sidecar opts in with `replay-user-messages`). Only that replay binds the
// attempt to its native turn id: assistant, tool and result frames, ordinary
// user rows, and replays of uuids we did not send are never identity evidence.

export const MAX_PENDING_CHECKPOINT_ATTEMPTS = 128

export function isUuid(value) {
return typeof value === 'string'
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
}

/** The checkpoint attempt of a `send` request, or undefined when it is absent or incomplete. */
export function checkpointAttemptOf(raw) {
return raw && isUuid(raw.uuid)
&& typeof raw.logicalAttemptId === 'string' && raw.logicalAttemptId.length > 0
&& typeof raw.requestId === 'string' && raw.requestId.length > 0
? { uuid: raw.uuid, logicalAttemptId: raw.logicalAttemptId, requestId: raw.requestId }
: undefined
}

/**
* Remember an attempt until its replay arrives. The set is bounded: an evicted
* attempt simply never gets an identity, which is never approximated by
* request order or prompt text.
*/
export function rememberCheckpointAttempt(pending, attempt) {
pending.delete(attempt.uuid)
pending.set(attempt.uuid, attempt)
while (pending.size > MAX_PENDING_CHECKPOINT_ATTEMPTS) {
pending.delete(pending.keys().next().value)
}
}

/** The `sdk.checkpointTurnIdentity` frame this SDK message proves, consuming its attempt; else undefined. */
export function takeCheckpointIdentity(pending, sessionId, msg) {
if (msg?.type !== 'user' || msg.isReplay !== true || msg.message?.role !== 'user' || !isUuid(msg.uuid)) return undefined
const attempt = pending.get(msg.uuid)
if (!attempt) return undefined
pending.delete(msg.uuid)
return {
type: 'sdk.checkpointTurnIdentity',
sessionId,
logicalAttemptId: attempt.logicalAttemptId,
requestId: attempt.requestId,
nativeTurnId: msg.uuid,
displayTurnId: msg.uuid,
}
}
3 changes: 2 additions & 1 deletion crates/freshell-claude-sidecar/exec-profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { spawn } from 'node:child_process'
import { openSync, readSync, closeSync, statSync, accessSync, constants } from 'node:fs'
import { endianness } from 'node:os'
import path from 'node:path'
import { usesHostedClaudeRuntime } from './hosted.mjs'

const fail = kind => { throw new Error(`provider exec profile ${kind}: repair inputs before dispatch`) }
function readBounded(file, limit) {
Expand Down Expand Up @@ -79,7 +80,7 @@ export function checkSdkExec(options) {
}

export function managedSdkSpawn(stderr, environment = process.env) {
if (process.platform !== 'linux' || environment.FRESHELL_HOSTED_FRESH_AGENT !== 'claude') return undefined
if (process.platform !== 'linux' || !usesHostedClaudeRuntime(environment)) return undefined
return options => {
checkSdkExec(options)
const child = spawn(options.command, options.args, {
Expand Down
4 changes: 3 additions & 1 deletion crates/freshell-claude-sidecar/hosted-mcp.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { usesHostedClaudeRuntime } from './hosted.mjs'

// The managed launch context selects one fixed recipe. Scoped authentication
// stays in the inherited child environment and never enters SDK argv/IPC.
export function hostedMcpServers(selected, env = process.env) {
if (selected !== true || env.FRESHELL_HOSTED_FRESH_AGENT !== 'claude') return undefined
if (selected !== true || !usesHostedClaudeRuntime(env)) return undefined
return { freshell: { command: 'node', args: ['/opt/freshell-mcp/server.js'] } }
}
25 changes: 25 additions & 0 deletions crates/freshell-claude-sidecar/hosted.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// hosted.mjs — whether this sidecar runs inside a hosted session host, and
// which hosted rules apply. The ONE reader of FRESHELL_HOSTED_FRESH_AGENT:
// the session host sets it to the provider it hosts, which for this runtime
// is `claude` (freshclaude) or `kilroy` (crates/freshell-supervisor/src/
// backend.rs). Two rules read it, and they differ on purpose:
// - every hosted sidecar logs CLI output by size only (isHostedSidecar);
// - the managed spawn check and the fixed hosted MCP recipe apply to hosted
// claude alone (usesHostedClaudeRuntime). Extending them to hosted kilroy
// is an explicit item of the api-hosted-ops workstream
// (docs/plans/2026-10-22-freshclaude-api-hosted-ops.md §2.4).

/** The provider the session host hosts, or undefined outside a session host. */
export function hostedProvider(env = process.env) {
return env.FRESHELL_HOSTED_FRESH_AGENT || undefined
}

/** Any hosted sidecar, freshclaude or kilroy: its CLI output is logged by size only. */
export function isHostedSidecar(env = process.env) {
return hostedProvider(env) !== undefined
}

/** Hosted claude: the SDK spawn is checked against the managed exec profile and the fixed MCP recipe is offered. */
export function usesHostedClaudeRuntime(env = process.env) {
return hostedProvider(env) === 'claude'
}
Loading
Loading