Skip to content

[3278 by Claude Opus 5.5] Avoid duplicate PROPR_INSTANCE_ID in hosted API container - #3279

Merged
integry merged 2 commits into
mainfrom
3278/claude-opus-5-5-avoid-duplicate-propr_ins-20261020-0020-gwl
Oct 20, 2026
Merged

integry merged 2 commits into
mainfrom
3278/claude-opus-5-5-avoid-duplicate-propr_ins-20261020-0020-gwl

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

AI Implementation Summary

Closes #3278

Branch: 3278/claude-opus-5-5-avoid-duplicate-propr_ins-20261020-0020-gwl
Commits: ✅ Changes committed (e5648d1)


AI Processing Completed

Execution Details:

Summary:
I've fixed it: the hosted API container now gets exactly one PROPR_INSTANCE_ID entry. The new regression test passes, and the existing launcher tests (135), CLI typecheck/lint and the tunnel command tests (35) are all green.

Cause: every app container already gets the id from the shared --env-file. tunnelApiEnvArgs() then always added -e PROPR_INSTANCE_ID=<lowercased id> for the API, and Docker records both entries without merging them.

Fix (docker/launcher/orchestrator.mjs):

  • New helper dockerEnvFileBindings() reads the env file the way the Docker CLI does: the value is taken exactly as written, with no quote, comment or export handling. The launcher's own parser does strip quotes, so it can't show what the container will actually see.

  • Config: resolveConfig() records those values as envFileProprInstanceIdBindings.

  • Skip rule: tunnelApiEnvArgs() leaves out the -e override only when the env file already provides exactly the lowercased, trimmed id. In every other case it still adds the override, so lowercasing, validation, tunnel URL derivation and local (no-tunnel) behaviour are unchanged. Those cases are:

    • a mixed-case value
    • a quoted value, which Docker would pass through with the quotes
    • an id set only in the process environment, or one that differs from the env file
    • an env file the launcher can't check

    The env file itself is not modified, so the other services still get the id from it.

  • Type: I added the optional field to packages/cli/src/orchestrator/types.ts. The CLI's copy of the orchestrator is a gitignored build copy; I refreshed it locally only.

Regression test: test/orchestratorInstanceIdBinding.test.mjs builds each container's final environment from its docker run arguments (env-file entries plus -e entries, not deduplicated, as Docker stores them). It checks that:

  • the hosted propr-api and every other app service have exactly one PROPR_INSTANCE_ID=abc123.
  • the derived tunnel/public API URLs are unchanged.
  • each override case above still adds the lowercased -e binding.
  • a local stack gets no binding.

The two exactly-one tests fail against the unmodified launcher, as expected.

Things to know:

  • Mixed-case or quoted ids still give the API two entries. Lowercasing has to override the file's value, and the file can't be changed. Fleet would still reject those cases; canonical lowercase ids, the normal case, now produce one entry.
  • Connect host-config path unchanged: when the CLI resolves config from pre-parsed env values, the exact Docker values aren't available, so the override is still added there. As far as I can see that path only inspects tunnel status and doesn't start containers.

As the issue says, integry/propr-fleet needs to refresh its pinned launcher fixture before the blocked qualification provision is retried. There's nothing visual in this change, so I made no previews.

Detailed Logs:

  • Conversation: 75 messages
  • Session: f8090297-a581-473b-b912-54dcc5e3f31b

Log files stored at:

  • conversation: /usr/src/app/logs/executions/issue-3278-2026-10-20T00-25-49-385Z-conversation.json
  • output: /usr/src/app/logs/executions/issue-3278-2026-10-20T00-25-49-385Z-output.txt
Latest Conversation Messages
ASSISTANT: [content unavailable]

ASSISTANT: I've fixed it: the hosted API container now gets exactly one `PROPR_INSTANCE_ID` entry. The new regression test passes, and the existing launcher tests (135), CLI typecheck/lint and the tunnel command...


This PR was created automatically by ProPR after processing issue #3278.


💡 Need changes?

Comment on this PR to request refinements — the AI agent monitors comments and will update the implementation based on your feedback. Keep iterating until you're satisfied!

…NSTANCE_ID` entry. The new regression test passes, and the existing launcher tests (135), CLI typecheck/lint and the tunnel command tests (35) are all green.

Cause: every app container already gets the id from the shared `--env-file`. `tunnelApiEnvArgs()` then always added `-e PROPR_INSTANCE_ID=<lowercased id>` for the API, and Docker records both entries without merging them.

Fix (`docker/launcher/orchestrator.mjs`):
- New helper `dockerEnvFileBindings()` reads the env file the way the Docker CLI does: the value is taken exactly as written, with no quote, comment or `export` handling. The launcher's own parser does strip quotes, so it can't show what the container will actually see.
- Config: `resolveConfig()` records those values as `envFileProprInstanceIdBindings`.
- Skip rule: `tunnelApiEnvArgs()` leaves out the `-e` override only when the env file already provides exactly the lowercased, trimmed id. In every other case it still adds the override, so lowercasing, validation, tunnel URL derivation and local (no-tunnel) behaviour are unchanged. Those cases are:
  - a mixed-case value
  - a quoted value, which Docker would pass through with the quotes
  - an id set only in the process environment, or one that differs from the env file
  - an env file the launcher can't check

  The env file itself is not modified, so the other services still get the id from it.
- Type: I added the optional field to `packages/cli/src/orchestrator/types.ts`. The CLI's copy of the orchestrator is a gitignored build copy; I refreshed it locally only.

Regression test: `test/orchestratorInstanceIdBinding.test.mjs` builds each container's final environment from its `docker run` arguments (env-file entries plus `-e` entries, not deduplicated, as Docker stores them). It checks that:
- the hosted `propr-api` and every other app service have exactly one `PROPR_INSTANCE_ID=abc123`.
- the derived tunnel/public API URLs are unchanged.
- each override case above still adds the lowercased `-e` binding.
- a local stack gets no binding.

The two exactly-one tests fail against the unmodified launcher, as expected.

Things to know:
- Mixed-case or quoted ids still give the API two entries. Lowercasing has to override the file's value, and the file can't be changed. Fleet would still reject those cases; canonical lowercase ids, the normal case, now produce one entry.
- Connect host-config path unchanged: when the CLI resolves config from pre-parsed env values, the exact Docker values aren't available, so the override is still added there. As far as I can see that path only inspects tunnel status and doesn't start containers.

As the issue says, `integry/propr-fleet` needs to refresh its pinned launcher fixture before the blocked qualification provision is retried. There's nothing visual in this change, so I made no previews.
@integry

integry commented Oct 20, 2026

Copy link
Copy Markdown
Owner

/review codex-astra

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex-astra

Overall Evaluation

The PR fixes duplicate identity injection for the usual lowercase hosted configuration, but needs changes to satisfy the visible requirement of exactly one binding while preserving normalization.

✅ Common hosted case fixed — A single canonical env-file binding suppresses the redundant API -e argument.

✅ Literal values distinguished — The new parser preserves quotes and value whitespace, avoiding confusion between Docker’s literal input and normalized configuration.

The issue description is truncated; this assessment uses its visible requirements. Current checks show no failures, with 22 pending. Review was static; no commands were run.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F1: 🔴 Normalization still produces duplicate identity bindings

  • Required behavior: Every managed service must expose exactly one PROPR_INSTANCE_ID entry while preserving existing lowercasing behavior.

  • Evidence: docker/launcher/orchestrator.mjs, tunnelApiEnvArgs; test/orchestratorInstanceIdBinding.test.mjs, mixed-case regression test.

    1. A hosted env file supplies the valid identity PROPR_INSTANCE_ID=AbC123.
    2. Configuration retains that identity and derives the lowercase public URL. The new literal-binding parser records AbC123.
    3. tunnelApiEnvArgs computes abc123; its equality check fails and it appends -e PROPR_INSTANCE_ID=abc123 alongside the shared env file.
    4. The resulting environment has two identity bindings, leaving this supported configuration subject to the same duplicate-binding admission rejection the PR is intended to fix.

    Validation permits mixed-case identities, so it does not prevent this sequence. The added test explicitly expects both bindings, thereby accepting the requirement violation. Quoted identities and differing process-environment overrides follow the same branch; multiple identical assignments also remain duplicated because the suppression condition accepts any positive array length.

    static trace: verified directly from the changed condition and supplied test expectations; no tests were executed.

  • Minimum fix: Construct the API’s effective environment input with exactly one canonical identity, including when normalization or an override is necessary, while preserving the shared env file for other services. Update the mixed-case, quoted-value, and differing-override tests to assert a single canonical binding.

Suggestions

These are optional follow-ups and are not sent to /fix.

S1: 🟢 Verify Docker’s recorded environment

An optional Docker-backed regression checking .Config.Env would strengthen confidence in the fix. The current helper models Docker behavior itself and differs from the production parser in details such as BOM handling. The unit tests remain useful, but an integration check would independently verify the external behavior motivating this change.

Score

The common hosted case is addressed with focused changes, but supported normalization cases still violate the central uniqueness requirement. Pending checks are not failures, although their completion remains necessary for final validation.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 11s
  • Tokens: 30,560 (29,584 in / 976 out)
  • Cost: $0.23

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F1 S1

🤖 Review by ProPR

@integry

integry commented Oct 20, 2026

Copy link
Copy Markdown
Owner

/fix F1

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 1866381

Addressed 1 AI review comment (IDs: 6280412206)

Addressed finding F1

Summary of Changes

I fixed F1: the hosted propr-api container now gets exactly one PROPR_INSTANCE_ID, the lowercased id. That holds for mixed-case and quoted values too, and when the process environment overrides the file.

Why the old approach couldn't work: the shared env file can't be changed, so when it held a non-canonical value, any -e override became a second entry.

The fix (docker/launcher/orchestrator.mjs, new apiIdentityEnv()): the launcher reads the shared env file once, the same way Docker does. That one read both decides what to do and supplies the content for the copy below.

  • File already has exactly the lowercased id: the API uses the shared file as is, with no -e.
  • File has no binding: the API uses the shared file plus one -e with the lowercased id. This is unchanged.
  • Anything else (mixed case, quotes, a repeated line, trailing whitespace, a bare PROPR_INSTANCE_ID line, or a differing process-env override): the API gets a private copy of the shared file with its PROPR_INSTANCE_ID lines removed and every other line kept exactly. It then gets one -e with the lowercased id.
    • The copy holds the stack's secrets, so it goes in a private temp directory (owner-only access) and the file itself is owner read/write only.
    • startService and startServiceAsync delete it in a finally once docker run has read it, including when the run fails.

The shared env file is never modified, and the other services keep using it. Tunnel URL derivation and the local (no-tunnel) behaviour are unchanged. In packages/cli/src/orchestrator/types.ts, the config field from the earlier commit is replaced by a flag, envFileDockerLiteral, saying whether the env file can be read as Docker reads it.

Tests (test/orchestratorInstanceIdBinding.test.mjs): the mixed-case, quoted and differing-override tests now expect a single lowercased entry. I added cases for:

  • repeated identical lines
  • a trailing-space value
  • a bare line resolved from the docker CLI's environment
  • the private copy: other lines kept exactly, owner-only permissions, shared file untouched, other services still using the shared file
  • a startService run against a stand-in docker on PATH, showing what docker run read and that the copy is deleted afterwards

Results:

  • All 11 tests in that file pass. Against the previous launcher, 7 of them fail, as expected.
  • The launcher tests (test/orchestrator*.test.mjs, 140) pass, and so do the hosted-tunnel config tests.
  • CLI typecheck and eslint are clean. The CLI tunnel, connect and hostActions tests pass. hostActions needs --experimental-test-module-mocks to run at all.

Separate from F1, not changed: in two cases the launcher still can't see what Docker will read, so it keeps the old behaviour (shared file plus -e) and a duplicate is possible:

  • Pre-parsed env values: when the CLI passes these in (the connect host-config path), the launcher doesn't read the file the way Docker does. As far as I can tell that path doesn't start containers.
  • Unreadable env file: docker run would fail on that file anyway.

🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @integry
  • Time: 5m 28s
  • Tokens: 1,457,892 (1,438,370 in / 19,522 out)
  • Cost: $1.04

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6280417728✓

…ly one `PROPR_INSTANCE_ID`, the lowercased id. That holds for mixed-case and quoted values too, and when the process environment overrides the file.

I fixed F1: the hosted `propr-api` container now gets exactly one `PROPR_INSTANCE_ID`, the lowercased id. That holds for mixed-case and quoted values too, and when the process environment overrides the file.

**Why the old approach couldn't work:** the shared env file can't be changed, so when it held a non-canonical value, any `-e` override became a second entry.

**The fix** (`docker/launcher/orchestrator.mjs`, new `apiIdentityEnv()`): the launcher reads the shared env file once, the same way Docker does. That one read both decides what to do and supplies the content for the copy below.
- **File already has exactly the lowercased id:** the API uses the shared file as is, with no `-e`.
- **File has no binding:** the API uses the shared file plus one `-e` with the lowercased id. This is unchanged.
- **Anything else** (mixed case, quotes, a repeated line, trailing whitespace, a bare `PROPR_INSTANCE_ID` line, or a differing process-env override): the API gets a private copy of the shared file with its `PROPR_INSTANCE_ID` lines removed and every other line kept exactly. It then gets one `-e` with the lowercased id.
  - The copy holds the stack's secrets, so it goes in a private temp directory (owner-only access) and the file itself is owner read/write only.
  - `startService` and `startServiceAsync` delete it in a `finally` once `docker run` has read it, including when the run fails.

The shared env file is never modified, and the other services keep using it. Tunnel URL derivation and the local (no-tunnel) behaviour are unchanged. In `packages/cli/src/orchestrator/types.ts`, the config field from the earlier commit is replaced by a flag, `envFileDockerLiteral`, saying whether the env file can be read as Docker reads it.

**Tests** (`test/orchestratorInstanceIdBinding.test.mjs`): the mixed-case, quoted and differing-override tests now expect a single lowercased entry. I added cases for:
- repeated identical lines
- a trailing-space value
- a bare line resolved from the docker CLI's environment
- the private copy: other lines kept exactly, owner-only permissions, shared file untouched, other services still using the shared file
- a `startService` run against a stand-in `docker` on `PATH`, showing what `docker run` read and that the copy is deleted afterwards

**Results:**
- All 11 tests in that file pass. Against the previous launcher, 7 of them fail, as expected.
- The launcher tests (`test/orchestrator*.test.mjs`, 140) pass, and so do the hosted-tunnel config tests.
- CLI typecheck and eslint are clean. The CLI tunnel, connect and `hostActions` tests pass. `hostActions` needs `--experimental-test-module-mocks` to run at all.

**Separate from F1, not changed:** in two cases the launcher still can't see what Docker will read, so it keeps the old behaviour (shared file plus `-e`) and a duplicate is possible:
- **Pre-parsed env values:** when the CLI passes these in (the connect host-config path), the launcher doesn't read the file the way Docker does. As far as I can tell that path doesn't start containers.
- **Unreadable env file:** `docker run` would fail on that file anyway.

PR: #3279
Comment by: @integry (ID: 6280417728)
Model: Claude Opus 5.5
@integry

integry commented Oct 20, 2026

Copy link
Copy Markdown
Owner

/review codex-astra

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex-astra

Overall Evaluation

The PR removes the redundant API identity argument when the shared env file already contains the canonical value, and uses a private filtered copy when normalization requires an override. Ready to merge within the reviewed scope, conditional on pending checks completing successfully.

✅ Canonical binding preserved — A single canonical env-file binding produces no additional PROPR_INSTANCE_ID argument.

✅ Shared configuration protected — Noncanonical API bindings are filtered into a private file with mode 0600, leaving the shared file unchanged.

✅ Lifecycle cleanup covered — Both synchronous and asynchronous starts remove scratch files in finally blocks around container removal and launch.

This was a static review of the supplied code; no commands or tests were run. The issue specification is explicitly truncated, so conclusions cover its visible requirements. The supplied current checks show no failures, with 22 still pending.

Merge blockers

No merge blockers.

Suggestions

These are optional follow-ups and are not sent to /fix.

S2: 🟢 Remove test fixture directories

test/orchestratorInstanceIdBinding.test.mjs creates env-file fixture directories and a fake Docker directory without removing them. Registering cleanup with t.after() would prevent repeated test runs from accumulating temporary files. This is optional test hygiene and does not affect production correctness.

S3: 🟢 Exercise failure cleanup paths

The new cleanup test covers successful synchronous startup. Additional tests for failed container launch and asynchronous startup would strengthen confidence that scratch files containing stack secrets are removed on those paths. The implementation already places cleanup in both finally blocks, so this is additional coverage rather than a demonstrated defect.

Score

The implementation addresses the visible hosted duplication problem with focused changes and useful regression coverage. Static inspection found no introduced merge blocker; final CI results remain pending.

Score: 9/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 31s
  • Tokens: 33,701 (33,101 in / 600 out)
  • Cost: $0.23

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix S2 S3

🤖 Review by ProPR

@integry
integry merged commit b59aa54 into main Oct 20, 2026
50 checks passed
@integry
integry deleted the 3278/claude-opus-5-5-avoid-duplicate-propr_ins-20261020-0020-gwl branch October 20, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid duplicate PROPR_INSTANCE_ID in hosted API container

1 participant