Repository navigation
Conversation
|
Reviewed 79ff421. Remote script publication can lose a recording when the caller's target file already exists, and the proxy help and docs still say remote publication is refused. When The proxy command help still says remote requests refuse script publication (command-overrides.ts:163), and remote-proxy.md:106-112 says the same and points to a local daemon. This PR removes that refusal for Not blocking, take or leave: remote versus local publication is inferred from the client's Could remote support be a No checks have reported on this draft head. The description says |
|
Note 🤖 gpt-6.1-sol responding on behalf of bradenbiz Good point that consuming publication before a caller-file collision breaks normal recovery; 081d14b makes The docs fix and simplification also use the shared capability parser, daemon-side allocation, explicit remote delivery, and registration-owned retention while preserving local HTTP behavior. The controlled Linux CI comparison ran the tenant-upload test on both unchanged base The exact-head local gate passed its code checks but stopped at Android SDK configuration during packaging, reproduced with the same command on clean base; will you please approve the PR’s CI workflow runs so the full upstream CI can run? |
|
This PR is ready at 2be928f. It fixes the problems in the earlier review of 79ff421, and I found no remaining problems in the diff. Not blocking, and you can take or leave these: /health hand-writes No conflicts. I judged this from the diff and did not run tests or a live remote proxy run. Upstream CI has not reported on 2be928f yet. You report The PR is still a draft. Please mark it ready for review when you consider it done, so the upstream checks can run. |
There was a problem hiding this comment.
17 issues found across 43 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/daemon/src/server/http/http-authorization-script-admission.test.ts">
<violation number="1" location="packages/daemon/src/server/http/http-authorization-script-admission.test.ts:46">
P3: The refusal cases all share one title, so a failure does not say which input carrier was accepted. Add a per-row label (for example a `name` field) or a row index placeholder such as `%#` to the title.</violation>
</file>
<file name="website/docs/docs/remote-proxy.md">
<violation number="1" location="website/docs/docs/remote-proxy.md:107">
P3: This sentence is unqualified, but the macOS app lease section refuses `open --save-script`. Name that exception here so the two sections agree.</violation>
</file>
<file name="docs/adr/0016-active-session-script-publication.md">
<violation number="1" location="docs/adr/0016-active-session-script-publication.md:91">
P2: This exception contradicts the unchanged failure list in the Artifact contract ("The recording is ABORTED or PUBLISHED") and the validation bullet "a repeated `session save-script` fails". Update those statements and the Rules at a glance so the remote PUBLISHED re-download case is stated once, consistently.</violation>
<violation number="2" location="docs/adr/0016-active-session-script-publication.md:92">
P3: "Remote recording" and the retained artifact re-download are not defined anywhere in this ADR. Add a dated amendment that defines the remote case and its retention, following the convention the other lifecycle changes use.</violation>
</file>
<file name="test/integration/provider-scenarios/remote-script-publication.test.ts">
<violation number="1" location="test/integration/provider-scenarios/remote-script-publication.test.ts:177">
P2: This negative check passes for any non-200 status, including a 500 or 404. Assert the auth refusal directly with `assert.equal(denied.status, 401)` so a broken auth gate on `/artifacts/<id>` cannot pass.</violation>
</file>
<file name="packages/remote/src/artifact-download.ts">
<violation number="1" location="packages/remote/src/artifact-download.ts:248">
P3: The hint is attached to every failure, but `--force` only helps when the reason is `script_target_exists`. For `script_download_failed` it points users at the wrong fix. Make the hint depend on `reason`.</violation>
</file>
<file name="packages/daemon/src/core/session-action-recorder.ts">
<violation number="1" location="packages/daemon/src/core/session-action-recorder.ts:76">
P2: The client delivery metadata passed here is dropped when no path is given. `resolveScriptTarget` returns early on `requested.path === undefined` without applying `clientPath` or `remote`, so a bare `--save-script` or `session save-script` over remote never produces a downloadable `session-script` artifact. If remote export should work without an explicit path, carry `clientPath` and `remote` through that early return. If default-target remote export is unsupported, reject it explicitly instead of silently ignoring the flag.</violation>
</file>
<file name="packages/daemon-client/src/daemon-client-rpc.ts">
<violation number="1" location="packages/daemon-client/src/daemon-client-rpc.ts:217">
P3: This save-script predicate is now duplicated in `sendHttpRequest` (daemon-client-transport.ts:800). Extract it into one helper, such as `requestNeedsActiveScriptArtifacts(req)`, and call it from both places so the capability gate and the network-access header cannot drift apart.</violation>
<violation number="2" location="packages/daemon-client/src/daemon-client-rpc.ts:225">
P3: The hint only names the upstream health response, but a direct daemon with no `upstream` field advertises `active-script-artifacts` at the top level. Reword the hint to say the capability must appear in the health response the client reads, or in the upstream response when a proxy forwards it.</violation>
</file>
<file name="packages/remote/src/__tests__/daemon-artifacts-script-download.test.ts">
<violation number="1" location="packages/remote/src/__tests__/daemon-artifacts-script-download.test.ts:26">
P3: The fake server never checks the requested path, so the test would pass even if the client downloaded a different artifact. Assert that `req.url` ends with `/script-id` (or matches the expected artifact route) in the handler.</violation>
</file>
<file name="packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts">
<violation number="1" location="packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts:376">
P3: This test's name says it covers regeneration from later actions, but the fixture has no actions after `publishedActionCount: 3`. Add a fourth recorded action after the publication point so the test proves later actions do not regenerate the expired artifact.</violation>
</file>
<file name="packages/remote/src/daemon-artifacts.ts">
<violation number="1" location="packages/remote/src/daemon-artifacts.ts:130">
P3: Empty `session save-script ""` skips the daemon's `cannot be empty` guard: the client strips the positional and resolves the empty path to the cwd directory. Reject a blank path here before publishing.</violation>
<violation number="2" location="packages/remote/src/daemon-artifacts.ts:468">
P1: `artifact.force` can authorize overwriting a different caller destination during the first export. Preserve the force grant only when it applies to this request’s destination, or have the daemon compute force per caller path.</violation>
<violation number="3" location="packages/remote/src/daemon-artifacts.ts:496">
P3: The doc comments above `finishesEarlierClientCapture` and `resolveMaterializedArtifactPath` still describe only stop and report requests as accepting an echoed `localPath`. Update them to cover `session save-script` as well, since this case now accepts it.</violation>
<violation number="4" location="packages/remote/src/daemon-artifacts.ts:497">
P2: `session save-script` trusts a daemon-returned `localPath` as the current caller’s output path. Use the current request’s `clientArtifactPaths` or resolve only a safe basename beneath the current caller cwd; do not treat persisted remote paths as local destinations.</violation>
</file>
<file name="packages/daemon-contracts/src/daemon-resolution.ts">
<violation number="1" location="packages/daemon-contracts/src/daemon-resolution.ts:151">
P2: This capability addition changes the published `daemon.json` shape and breaks the existing quiesce runtime test, which still expects only `['quiesce']`. Update that assertion (and related fixture expectations) to include `active-script-artifacts`.</violation>
</file>
<file name="packages/daemon/src/resources/artifact-tracking.ts">
<violation number="1" location="packages/daemon/src/resources/artifact-tracking.ts:331">
P2: Retained directory registrations lose their retention flag here, so the HTTP handler treats the generated tarball as consumable and deletes the source after the first download. Propagate `entry.retainAfterDownload` in the directory return as well.</violation>
</file>
Reply to a comment to ask cubic a question or push back. It learns from your replies.
View guided diff | Re-trigger cubic
| // `downloadRemoteArtifact`). | ||
| isDirectory: artifact.artifactType === 'test-artifacts', | ||
| ...(artifact.artifactType === 'session-script' | ||
| ? { scriptForce: req.flags?.force === true || artifact.force === true } |
There was a problem hiding this comment.
P1: artifact.force can authorize overwriting a different caller destination during the first export. Preserve the force grant only when it applies to this request’s destination, or have the daemon compute force per caller path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/daemon-artifacts.ts, line 468:
<comment>`artifact.force` can authorize overwriting a different caller destination during the first export. Preserve the force grant only when it applies to this request’s destination, or have the daemon compute force per caller path.</comment>
<file context>
@@ -447,11 +464,17 @@ export async function materializeRemoteArtifacts(
// `downloadRemoteArtifact`).
isDirectory: artifact.artifactType === 'test-artifacts',
+ ...(artifact.artifactType === 'session-script'
+ ? { scriptForce: req.flags?.force === true || artifact.force === true }
+ : {}),
});
</file context>
| Thus, the caller can correct the target or permissions and retry. | ||
| ABORTED and PUBLISHED are terminal until the session is destroyed. | ||
| In either state, `session save-script` fails with an explicit error and never writes. | ||
| In either state, `session save-script` never records or publishes new script content. |
There was a problem hiding this comment.
P2: This exception contradicts the unchanged failure list in the Artifact contract ("The recording is ABORTED or PUBLISHED") and the validation bullet "a repeated session save-script fails". Update those statements and the Rules at a glance so the remote PUBLISHED re-download case is stated once, consistently.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/adr/0016-active-session-script-publication.md, line 91:
<comment>This exception contradicts the unchanged failure list in the Artifact contract ("The recording is ABORTED or PUBLISHED") and the validation bullet "a repeated `session save-script` fails". Update those statements and the Rules at a glance so the remote PUBLISHED re-download case is stated once, consistently.</comment>
<file context>
@@ -88,7 +88,11 @@ An ordinary recording eligible for active-session publication has three states:
Thus, the caller can correct the target or permissions and retry.
ABORTED and PUBLISHED are terminal until the session is destroyed.
-In either state, `session save-script` fails with an explicit error and never writes.
+In either state, `session save-script` never records or publishes new script content.
+A PUBLISHED remote recording may return its retained immutable artifact for another caller-side download.
+This exception does not change the terminal recording state.
</file context>
| assert.ok(artifact?.artifactId); | ||
| assert.equal(artifact.path, undefined); | ||
| const denied = await fetch(`${proxy.baseUrl}/artifacts/${artifact.artifactId}`); | ||
| assert.notEqual(denied.status, 200); |
There was a problem hiding this comment.
P2: This negative check passes for any non-200 status, including a 500 or 404. Assert the auth refusal directly with assert.equal(denied.status, 401) so a broken auth gate on /artifacts/<id> cannot pass.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At test/integration/provider-scenarios/remote-script-publication.test.ts, line 177:
<comment>This negative check passes for any non-200 status, including a 500 or 404. Assert the auth refusal directly with `assert.equal(denied.status, 401)` so a broken auth gate on `/artifacts/<id>` cannot pass.</comment>
<file context>
@@ -0,0 +1,191 @@
+ assert.ok(artifact?.artifactId);
+ assert.equal(artifact.path, undefined);
+ const denied = await fetch(`${proxy.baseUrl}/artifacts/${artifact.artifactId}`);
+ assert.notEqual(denied.status, 200);
+ const downloaded = await fetch(`${proxy.baseUrl}/artifacts/${artifact.artifactId}`, {
+ headers: { authorization: 'Bearer raw-client' },
</file context>
| assert.notEqual(denied.status, 200); | |
| assert.equal(denied.status, 401); |
| entry.flags, | ||
| hostCwd, | ||
| entry.clientScriptPath, | ||
| entry.remoteScriptDelivery, |
There was a problem hiding this comment.
P2: The client delivery metadata passed here is dropped when no path is given. resolveScriptTarget returns early on requested.path === undefined without applying clientPath or remote, so a bare --save-script or session save-script over remote never produces a downloadable session-script artifact. If remote export should work without an explicit path, carry clientPath and remote through that early return. If default-target remote export is unsupported, reject it explicitly instead of silently ignoring the flag.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/core/session-action-recorder.ts, line 76:
<comment>The client delivery metadata passed here is dropped when no path is given. `resolveScriptTarget` returns early on `requested.path === undefined` without applying `clientPath` or `remote`, so a bare `--save-script` or `session save-script` over remote never produces a downloadable `session-script` artifact. If remote export should work without an explicit path, carry `clientPath` and `remote` through that early return. If default-target remote export is unsupported, reject it explicitly instead of silently ignoring the flag.</comment>
<file context>
@@ -65,7 +67,14 @@ export function recordActionEntry(
+ entry.flags,
+ hostCwd,
+ entry.clientScriptPath,
+ entry.remoteScriptDelivery,
+ );
const fillLiteral = readRecordedFillLiteral(entry);
</file context>
|
|
||
| /** What a daemon can do beyond the base RPC contract, published in its `daemon.json`. */ | ||
| export const DAEMON_CAPABILITIES = ['quiesce'] as const; | ||
| export const DAEMON_CAPABILITIES = ['quiesce', 'active-script-artifacts'] as const; |
There was a problem hiding this comment.
P2: This capability addition changes the published daemon.json shape and breaks the existing quiesce runtime test, which still expects only ['quiesce']. Update that assertion (and related fixture expectations) to include active-script-artifacts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon-contracts/src/daemon-resolution.ts, line 151:
<comment>This capability addition changes the published `daemon.json` shape and breaks the existing quiesce runtime test, which still expects only `['quiesce']`. Update that assertion (and related fixture expectations) to include `active-script-artifacts`.</comment>
<file context>
@@ -148,7 +148,7 @@ export function resolveDaemonTransportPreference(
/** What a daemon can do beyond the base RPC contract, published in its `daemon.json`. */
-export const DAEMON_CAPABILITIES = ['quiesce'] as const;
+export const DAEMON_CAPABILITIES = ['quiesce', 'active-script-artifacts'] as const;
export type DaemonCapability = (typeof DAEMON_CAPABILITIES)[number];
</file context>
| if ( | ||
| info.baseUrl && | ||
| !info.capabilities?.includes('active-script-artifacts') && | ||
| (req.flags?.saveScript || req.command === INTERNAL_COMMANDS.sessionSaveScript) |
There was a problem hiding this comment.
P3: This save-script predicate is now duplicated in sendHttpRequest (daemon-client-transport.ts:800). Extract it into one helper, such as requestNeedsActiveScriptArtifacts(req), and call it from both places so the capability gate and the network-access header cannot drift apart.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon-client/src/daemon-client-rpc.ts, line 217:
<comment>This save-script predicate is now duplicated in `sendHttpRequest` (daemon-client-transport.ts:800). Extract it into one helper, such as `requestNeedsActiveScriptArtifacts(req)`, and call it from both places so the capability gate and the network-access header cannot drift apart.</comment>
<file context>
@@ -207,3 +208,22 @@ export function refuseOversizedDaemonRequest(payload: string, requestId: string
+ if (
+ info.baseUrl &&
+ !info.capabilities?.includes('active-script-artifacts') &&
+ (req.flags?.saveScript || req.command === INTERNAL_COMMANDS.sessionSaveScript)
+ ) {
+ throw new AppError(
</file context>
| if (existing) fs.writeFileSync(destination, 'original'); | ||
| const text = 'context platform=ios\nopen Demo\n'; | ||
| const server = http.createServer((req, res) => { | ||
| expect(req.headers.authorization).toBe('Bearer test'); |
There was a problem hiding this comment.
P3: The fake server never checks the requested path, so the test would pass even if the client downloaded a different artifact. Assert that req.url ends with /script-id (or matches the expected artifact route) in the handler.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/__tests__/daemon-artifacts-script-download.test.ts, line 26:
<comment>The fake server never checks the requested path, so the test would pass even if the client downloaded a different artifact. Assert that `req.url` ends with `/script-id` (or matches the expected artifact route) in the handler.</comment>
<file context>
@@ -0,0 +1,124 @@
+ if (existing) fs.writeFileSync(destination, 'original');
+ const text = 'context platform=ios\nopen Demo\n';
+ const server = http.createServer((req, res) => {
+ expect(req.headers.authorization).toBe('Bearer test');
+ expect(req.headers['x-agent-device-tenant']).toBe('tenant');
+ expect(req.headers['x-agent-device-lease-id']).toBe('lease');
</file context>
| expect(fs.existsSync(output)).toBe(true); | ||
| }); | ||
|
|
||
| test('an expired remote publication is not regenerated from later actions', () => { |
There was a problem hiding this comment.
P3: This test's name says it covers regeneration from later actions, but the fixture has no actions after publishedActionCount: 3. Add a fourth recorded action after the publication point so the test proves later actions do not regenerate the expired artifact.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts, line 376:
<comment>This test's name says it covers regeneration from later actions, but the fixture has no actions after `publishedActionCount: 3`. Add a fourth recorded action after the publication point so the test proves later actions do not regenerate the expired artifact.</comment>
<file context>
@@ -348,3 +348,50 @@ test('refuses mutating find steps that cannot enforce target identity on replay'
+ expect(fs.existsSync(output)).toBe(true);
+});
+
+test('an expired remote publication is not regenerated from later actions', () => {
+ const output = path.join(root, 'expired.ad');
+ store.publish(
</file context>
| function finishesEarlierClientCapture(req: DaemonWireRequest, artifact: DaemonArtifact): boolean { | ||
| const [first, second, third] = (req.positionals ?? []).map((value) => value.toLowerCase()); | ||
| switch (req.command) { | ||
| case INTERNAL_COMMANDS.sessionSaveScript: |
There was a problem hiding this comment.
P3: The doc comments above finishesEarlierClientCapture and resolveMaterializedArtifactPath still describe only stop and report requests as accepting an echoed localPath. Update them to cover session save-script as well, since this case now accepts it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/daemon-artifacts.ts, line 496:
<comment>The doc comments above `finishesEarlierClientCapture` and `resolveMaterializedArtifactPath` still describe only stop and report requests as accepting an echoed `localPath`. Update them to cover `session save-script` as well, since this case now accepts it.</comment>
<file context>
@@ -467,9 +490,11 @@ export async function materializeRemoteArtifacts(
+function finishesEarlierClientCapture(req: DaemonWireRequest, artifact: DaemonArtifact): boolean {
const [first, second, third] = (req.positionals ?? []).map((value) => value.toLowerCase());
switch (req.command) {
+ case INTERNAL_COMMANDS.sessionSaveScript:
+ return artifact.field === 'savedScript' && artifact.artifactType === 'session-script';
case 'record':
</file context>
| } | ||
| clientPaths.savedScript = localPath; | ||
| if (active) { | ||
| if (positionals.length === 1) positionals.length = 0; |
There was a problem hiding this comment.
P3: Empty session save-script "" skips the daemon's cannot be empty guard: the client strips the positional and resolves the empty path to the cwd directory. Reject a blank path here before publishing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/daemon-artifacts.ts, line 130:
<comment>Empty `session save-script ""` skips the daemon's `cannot be empty` guard: the client strips the positional and resolves the empty path to the cwd directory. Reject a blank path here before publishing.</comment>
<file context>
@@ -95,25 +97,40 @@ export async function prepareRemoteRequestArtifacts(
+ }
+ clientPaths.savedScript = localPath;
+ if (active) {
+ if (positionals.length === 1) positionals.length = 0;
+ return flags;
+ }
</file context>
| if (positionals.length === 1) positionals.length = 0; | |
| if (positionals.length === 1) { | |
| if (!positionals[0]?.trim()) { | |
| throw new AppError('INVALID_ARGS', 'session save-script path cannot be empty.'); | |
| } | |
| positionals.length = 0; | |
| } |
Summary
I use externally owned HTTP sessions and need to export journeys without closing them:
Use the same configured remote connection. A fresh
open --save-scriptarms ordinary recording.session save-scriptdownloads an authenticatedsession-scriptartifact; repeat it to retrieve the same immutable script after a collision or interrupted transfer. Use--forcefor an existing caller file. Publication does not close the session or change its owner/driver.The implementation uses
active-script-artifactsin the existing capability vocabulary, daemon-owned allocation, explicit remote delivery, registration-owned retention, and atomic caller writes. Older daemons refuse before recording RPC.Related: #1810. Close-time/repaired-script downloads remain excluded. Plain close discards an unpublished remote recording. The diff touches 43 files. One internal workspace dependency is declared for registry constants; no external package was added.
Validation
Head:
2be928f1041dfad40f4a97e662bb3c7ac50bbfac.pnpm check:affected --run --head HEAD: failed, exactCommit=true. Format, lint, types, layering, DI, Fallow, native-duplication checks, MCP metadata and build passed. Packaging stopped at missing Android SDK configuration; the same package command fails on clean baseb68ed4bf2. Remaining gates were not reached.12 provider scenarios and 50 wire/mutation tests passed; docs and released-wire compatibility passed. Independent review findings were fixed. No live devices used.
The tenant-upload test passes on both unchanged base and revised head in controlled Linux CI. Its local failure remains unexplained; full upstream CI is still pending.
Created with gpt-6.1-sol in the Codex harness through T3 Code.
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.