Skip to content

feat: env-file import for workspace credentials — one parser, two doors - #114

Merged
pythonlearner1025 merged 2 commits into
mainfrom
cred-import
Aug 29, 2026
Merged

feat: env-file import for workspace credentials — one parser, two doors#114
pythonlearner1025 merged 2 commits into
mainfrom
cred-import

Conversation

@pythonlearner1025

@pythonlearner1025 pythonlearner1025 commented Aug 29, 2026

Copy link
Copy Markdown
Member

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:

  • Agents: blitz-cred import [--check] [--label TEXT] FILE|- on any
    workspace-admin machine. The acting member is the machine's member,
    resolved at call time — same rule as the pull wire.
  • Humans: the credentials tab in the workspace details dialog gets an
    import block: paste or choose a file, watch a live preview, then import.

Both surfaces call the same import. The webApp preview and --check are the
same dryRun flag on the same request, so the preview is a promise: the same
parse, the same outcomes, no writes.

Wire

  • POST /workspaces/self/credentials/dotenv (box plane)
  • POST /workspaces/:id/credentials/dotenv (session plane)
  • Body { text, label?, dryRun? }{ results: [{ name, line, outcome, reason? }], linesRead }

Outcomes are store-level facts: stored, rotated (a live name's value
changed), unchanged (same value, nothing written — a re-run of the same
file 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, optional export , #
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 second
rotation semantics: one live row per name, the revoked row stays as the
audit trail, blitz-cred serves 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 produces
the 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 comment saying what the key is for.

  • blitz-cred list prints it after a #; agents read the comments to pick
    the 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 key
    with its comment (box-plane PUT /workspaces/self/credentials,
    workspace-admin gate, value on stdin so argv never holds a secret).
  • The credentials tab shows comments on rows and offers a comment field in
    the add-or-rotate form.
  • The comment is tri-state on the wire: absent keeps the live row's comment
    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.
  • Migration 0045 adds the column; new corpus
    packages/schema/fixtures/credential-list/ pins the list wire on both
    sides.

Docs

agent-rules.md (the baked ~/.claude/CLAUDE.md) gains a "Sharing secrets
with 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

pythonlearner1025 and others added 2 commits August 29, 2026 23:17
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
pythonlearner1025 merged commit 2aa3700 into main Aug 29, 2026
2 of 4 checks passed
@pythonlearner1025
pythonlearner1025 deleted the cred-import branch August 29, 2026 23:20
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
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.

1 participant