feat: env-file import for workspace credentials — one parser, two doors - #114
Merged
Conversation
An admin moves the keys in a dotenv file into the workspace credential store from either surface: `blitz-cred import [--check] FILE|-` on a workspace-admin machine, or the import block in the credentials tab. Both post the same body to the same import; the webApp preview and `--check` are one `dryRun` flag, so the preview is a promise. Wire: POST /workspaces/self/credentials/dotenv (box plane, acting member resolved at call time) and POST /workspaces/:id/credentials/dotenv (session plane), both behind the workspace-admin gate. Outcomes are store-level facts — stored, rotated, unchanged, refused — and no value ever travels back. Each key writes through putWorkspaceCredential, so import invents no second rotation semantics. New cross-runtime corpus packages/schema/fixtures/credential-import/, pinned on both sides. agent-rules.md gains "Sharing secrets with the workspace". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012R7NUDJXy3eUBeKFYTMt1A
A workspace credential can carry a comment. `blitz-cred list` prints it after a `#`, so an agent picking a key reads what each one is for instead of asking. The credentials tab shows it on rows and offers a field in the add-or-rotate form. Two new box-plane routes beside the pull wire: GET /workspaces/self/credentials (names and comments, never values — a new route rather than a new field, because deployed boxes decode the connection list with DisallowUnknownFields) and PUT /workspaces/self/credentials, the box-plane twin of the tab's save, behind the same workspace-admin gate. `blitz-cred put NAME --comment` drives it; the value arrives on stdin so argv never holds a secret. The comment is tri-state on the wire: absent keeps the live row's comment across a rotation — an env-file re-import cannot erase explanations — explicit null clears, a string sets. Import never reads or writes comments. Migration 0045 adds the column; the new corpus packages/schema/fixtures/credential-list/ pins the list wire on both sides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012R7NUDJXy3eUBeKFYTMt1A
pythonlearner1025
force-pushed
the
cred-import
branch
from
August 29, 2026 23:20
8e65dd6 to
b562c15
Compare
pythonlearner1025
added a commit
that referenced
this pull request
Aug 30, 2026
#114 added core/workspace-credential-import.ts and updated the module manifest and both module lists, but not the third hand-maintained number: the length assertion inside blitzdev-emitter's vendor-only describe. That block is skipIf'd without BLITZDEV_MANAGED=1, so a local `npm test` reported green and CI — which sets the variable — went red on main. This is the trap CLAUDE.md names under "Adding a file under core/ touches three hand-maintained lists". Verified the way that note prescribes: `BLITZDEV_MANAGED=1 npx vitest run` is 64 files and 686 tests green, against 59 and 648 without the variable. Claude-Session: https://claude.ai/code/session_012R7NUDJXy3eUBeKFYTMt1A Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pythonlearner1025
added a commit
that referenced
this pull request
Aug 30, 2026
main gained the memory boundary (#113), machine-stats (#112), env-file credential import (#114), the managed-count fix (#117), canary's R2 box image (#115) and the connections/mobile fixes. This branch is the Lody port, which deleted the box actor entirely. How each side was chosen: - `packages/box/actor/**` stays deleted. That was a direct user order and nothing here softens it. main's `actor/run` change is dropped with the service it wrapped. - machine-stats is actor-independent guest work, so it stays: the s6 service, `blitz-machine-stats` and the CP consumer land unchanged, and its conformance test moves to `packages/box/guest-tests/test/` the way the deletion commit re-homed every other guest test. The relative paths the test reads (`../../rootfs`, `../../../schema/fixtures`) resolve at the same depth, so nothing in it needed editing. - The memory boundary's one actor dependency was the placement of the process that hosts agents. The Lody daemon is that process now, so it inherits the scope: `lody-daemon/run` enters `user/lody.scope`, and `docs/MEMORY-BOUNDARY.md`, `blitz-cgroup` and `smoke.sh` say so. The daemon is dark by default, so smoke.sh reads the run script rather than looking for a node that is not there. - `.github/workflows/**` takes main's side whole — this branch never meant to touch canary.yml, and after the merge the directory is byte-identical to origin/main. - `CreateRecipeScreen.tsx` takes main's `cfg-` settings-surface styling and drops the chat-harness copy and `chatNeedsModel`, which have no referent on this branch. - `WorkspaceDetailsDialog.test.tsx` takes main's `IMPORT_PREVIEW_DEBOUNCE_MS` import beside this branch's `SessionRail` rename. - `shell-smoke.test.tsx`: main's new mobile-drawer test passed an `acp` port that `StandalonePorts` no longer has. Adding a file under `core/` touches three hand-maintained lists, and the merge is where two branches' additions meet. All three take the union: `worker-source.mjs` gains main's `workspace-credential-import.ts` beside this branch's `workspace-drain.ts`; `core-imports.test.ts` counts 109 (104 at the fork, +2 from main, +3 here); and the managed upload set in `blitzdev-emitter.test.ts` gains `core/wire-sharing.ts`, `core/session-shares.ts` and `core/workspace-drain.ts` to reach 112. Those three are 2.2 KB, 14 KB and 2.9 KB of source, so the emitter's 1 MiB-per-file platform limit still holds with four orders of magnitude to spare — and the test asserts it on every file, not just the new ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vk3ghWvYvp74ae9EUQPPkJ
pythonlearner1025
added a commit
that referenced
this pull request
Sep 1, 2026
The redesign branched at #110, three hours before #112 landed `settings-surface.css` — the `cfg-` canon CLAUDE.md now names as the one system for every settings-shaped screen. Both sides restyled the same surfaces, so the resolution keeps the redesign's STRUCTURE and main's VOCABULARY: primitives, the org switcher, the surface ladder and the danger-zone/About headings survive, expressed in `cfg-` classes rather than in a second set of headings, fields, dividers and fact lists. Per file: - settings.css — the redesign's ladder and its `.settings-switch-*` shape stay; every rule whose markup moved to the canon is gone (definition list, appearance wrapper, `.settings-field*`, `.settings-danger*`, `.settings-note`, `.settings-credential-section`, `.connect-field__label`). The account page keeps its red frame around the canon's `.cfg-danger`. - workspace-details-dialog.css — the ladder's raised cards stay; the heading, field and fact-list rules the canon owns are gone, and #114's credential import and #112's volume meter come across whole. The settings form's machine-type select re-targets `.cfg-field` and takes the one control height. - SettingsPage.tsx — main's `.cfg-meta` profile facts and `.cfg-section` appearance block, plus the redesign's Organizations section, re-headed with `.cfg-section-head` / `.cfg-title`. - MembersPanel.tsx — the redesign's named "Danger zone" over main's `.cfg-danger` / `.cfg-danger-action` zone. - UsagePanel.tsx — the redesign's `SettingsSwitch` (it writes on change, which is what the primitive is for) with main's `.cfg-help` notes. - WorkspaceSettingsTab.tsx — main's `.cfg-field--inline` checkbox, NOT `SettingsSwitch`: that row is part of a draft the Save below sends, and the primitive's own contract forbids a Save beside it. `cfg-actions` and `cfg-meta` as main has them, with the redesign's "About" title. - WorkspaceDetailsDialog.tsx — main's `.cfg-section` credentials tab and its comment field, with the redesign's red Revoke. - CloudApp.tsx — the redesign cuts the desktop statusline; #155's BUG-CV-02 sentence keeps one desktop surface, because a box the browser cannot reach has no other affordance. The bar now shows on desktop when the terminal needs its sign-in pair OR when the gateway is unreachable. Also carried across the version gap: `settings-surface.css` gains the disabled state the redesign gave its danger button, the preview fixtures gain the fields main added to `MachineView`, `WorkspaceCredentialView` and `CatalogEntryView`, the preview gallery passes `refreshWorkspaces`, and two tests drop props the redesign removed. Gates: typecheck and lint:gate pass at baseline (74 anti-slop, 0 house). `npm test` passes except `webapp/test/lody-worktree-composer.test.tsx`, which fails the same way on origin/main alone — a pre-existing failure, not this merge's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Env-file import for workspace credentials
One parser, two doors. An admin moves the keys in a dotenv file into the
workspace credential store, from either surface:
blitz-cred import [--check] [--label TEXT] FILE|-on anyworkspace-admin machine. The acting member is the machine's member,
resolved at call time — same rule as the pull wire.
import block: paste or choose a file, watch a live preview, then import.
Both surfaces call the same import. The webApp preview and
--checkare thesame
dryRunflag on the same request, so the preview is a promise: the sameparse, the same outcomes, no writes.
Wire
POST /workspaces/self/credentials/dotenv(box plane)POST /workspaces/:id/credentials/dotenv(session plane){ text, label?, dryRun? }→{ results: [{ name, line, outcome, reason? }], linesRead }Outcomes are store-level facts:
stored,rotated(a live name's valuechanged),
unchanged(same value, nothing written — a re-run of the samefile must not read as fifty rotations),
refused(per line, with a reason;the rest of the file still imports). No value ever travels back.
Parser
The least surprising slice of dotenv:
NAME=value, optionalexport,#comments, one matching pair of surrounding quotes. No escape expansion, no
line continuation — the pull wire refuses a newline anyway, so a multi-line
value (PEM, JSON key) is refused with "base64-encode it first" instead of
storing a fragment. A duplicated name keeps the last line and reports the
earlier one.
Storage
Each key goes through
putWorkspaceCredential, so import invents no secondrotation semantics: one live row per name, the revoked row stays as the
audit trail,
blitz-credserves the new value on the next pull.Contract
New cross-runtime corpus
packages/schema/fixtures/credential-import/(valid + invalid), pinned by
test/credential-import.test.ts(CP producesthe valid fixture byte for byte) and
broker/internal/workspace/credimport_test.go(Go consumer accepts valid,refuses print-unsafe bodies). CLAUDE.md contract table has the new row.
Comments (round two)
A credential can carry a one-line
commentsaying what the key is for.blitz-cred listprints it after a#; agents read the comments to pickthe right key. The read is a new box route
(
GET /workspaces/self/credentials, names and comments, never values),so deployed boxes with the old strict decoder keep working.
printf '%s' "$VALUE" | blitz-cred put NAME --comment "…"stores one keywith its comment (box-plane
PUT /workspaces/self/credentials,workspace-admin gate, value on stdin so argv never holds a secret).
the add-or-rotate form.
across a rotation — so an env-file re-import cannot erase explanations —
explicit null clears, a string sets. Import itself never reads or writes
comments.
packages/schema/fixtures/credential-list/pins the list wire on bothsides.
Docs
agent-rules.md(the baked~/.claude/CLAUDE.md) gains a "Sharing secretswith the workspace" section: import, check, the one-line rule, and the
delete-the-file-afterwards guidance.
🤖 Generated with Claude Code
https://claude.ai/code/session_012R7NUDJXy3eUBeKFYTMt1A