Skip to content

fix: a workspace value has one source; an empty delivered file is broken (r144) - #90

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/workspace-value-one-source
Oct 26, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/workspace-value-one-source

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #88, from review r144 (f1–f3), under the maximum-security posture. Every workspace value now has exactly one source, and every broken delivery is refused with a named reason.

f1: a pinned value's broken delivery is ErrAuthorityValueChanged everywhere. WorkspaceValue and WorkspaceValueIfSet now wrap a live read failure for a pinned name as ErrAuthorityValueChanged: <name> cannot be read: <cause>, which is how Authority.Recheck already reports it. #88 returned the bare ErrFileCarrier, so a caller that stops or re-seals on ErrAuthorityValueChanged missed a broken pinned delivery. Before #88, the base returned ErrAuthorityValueChanged without the cause. Both are now one helper, pinnedReadFailure.

f2: an empty delivered file is a broken delivery, never absence. readFileCarrier refuses a file carrier whose file is empty with ErrFileCarrier: <key>: delivered file is empty. Core delivers a value by file only when it exceeds resources.FileCarrierThreshold (deliverByFile), so an empty file is a truncated write or a mount race. It used to read as absent, so WorkspaceValue and ReadAuthority answered from the secret namespace: r144 probed an empty public file plus a secret sec1 and got "sec1" with a nil error. The check sits in the one function that reads carrier files, so every accessor and LoadEnvironmentVariables refuse it. Configuration documents report it as ErrConfigurationDocumentUnreadable. An empty inline value is still absent.

f3: a name in both namespaces is refused. workspaceValueIfSetLive reads both namespaces and refuses a name carried in both with the new exported ErrWorkspaceValueInBothNamespaces (<name>/<key>, never the value). That is two sources for one value, the same rule as inline plus file. It used to answer from the public namespace silently: r144 probed public pub plus secret sec and got "pub". A deployment that moved a value to the secret namespace and left the old public copy behind kept answering the stale copy. A read failure in either namespace is returned. Only true absence moves the lookup on. WorkspaceConfiguration and WorkspaceSecret each name one namespace and still answer it.

Docs:

  • README.md gains a Workspace values section, and the authority section names the f1 behaviour.
  • In AGENTS.md, the empty-value bullet now names the two refusals, within the 200-line budget (TestAgentContextRootStaysWithinItsBudget).
  • The runtime-carrier-accessor skill and docs/configuration-documents.md now say an empty delivered file is broken, not absent.

Fix or hack

Fix, at the owner. f1 and f3 change the single lookup #88 made for WorkspaceValue, WorkspaceValueIfSet, ReadAuthority and Recheck. f2 changes the single function that reads carrier files. Nothing new is added beside them.

Behaviour change for callers

  • A deployment that carries one name in both namespaces now gets a refusal from WorkspaceValue/WorkspaceValueIfSet/ReadAuthority instead of the public value. Remove one of the two copies.
  • A process whose environment names an empty delivered file now fails LoadEnvironmentVariables, the same as for a missing file.
  • r144 f4 (corrected by r154 f1): the affected consumer is codefly-dev/module-saas-starter, not module-saas-starter-security. Its workspaceEnv helpers, in auth-gateway configuration.go:13 and accounts work.go:2060, use err == nil && value != "" and otherwise fall back to os.Getenv. Its test TestWorkspaceEnvResolvesConfigurationSecretAndFallback (auth-gateway configuration_test.go:27-28) holds that fallback in place. accounts work.go also discards the error outright at :2088, :2108, :2158 and :2490 (value, _ := ...WorkspaceValue(...)); lines are as of module-saas-starter de039b2. These call sites turn the refusals from fix: an unreadable public value is refused, never answered from the secret namespace (r139) #88 and this PR (an unreadable delivery, an empty delivered file, a name in both namespaces) back into a silent fallback to a process variable. The SDK refuses correctly; the fail-open is in the consumer and is the consumer's to fix. No issue has been filed.

Test plan

  • TestAnUnreadablePublicValueIsNeverAnsweredFromTheSecretNamespace now asserts ErrAuthorityValueChanged plus ErrFileCarrier from WorkspaceValue and WorkspaceValueIfSet on a pinned name whose public delivery broke, with no value in the error. It fails with f1 reverted.
  • New TestAnEmptyDeliveredFileIsABrokenDelivery reproduces r144's sec1 probe. WorkspaceConfiguration, WorkspaceValue, WorkspaceValueIfSet and ReadAuthority all refuse with ErrFileCarrier, and so does LoadEnvironmentVariables. The listing's empty-file subtest now expects a named failure. Both fail with f2 reverted.
  • New TestAValueInBothNamespacesIsRefused reproduces r144's pub probe. WorkspaceValue, WorkspaceValueIfSet and ReadAuthority refuse with ErrWorkspaceValueInBothNamespaces, while the per-namespace accessors still answer. A value pinned from one namespace whose name then appears in the other is refused by Recheck and by WorkspaceValue as ErrAuthorityValueChanged. It fails with f3 reverted.
  • ./scripts/check-one-implementation.sh: ok (86 files). ./scripts/security-posture.sh: SP-WC-01, SP-WC-05 ok.
  • go test -race -count=1 ./... passes in the root (including the agent-context budget) and in workcontext.
  • Root coverage is 80.3% (gate 75). The new helpers are at 100%, and readFileCarrier is at 100%.
  • golangci-lint run ./...: 0 issues in both modules.

release/0.4

This goes to release/0.4 by cherry-pick once it merges, after #81's backport, which restores the retractions v0.4.2 lost.

Not verified

  • No service booted against a real runtime.

🤖 Generated with Claude Code

…ken (r144)

Three refusals under the maximum-security posture, from review r144:

- f1: WorkspaceValue and WorkspaceValueIfSet now report a pinned value whose
  live delivery breaks as ErrAuthorityValueChanged wrapping the reason, as
  Authority.Recheck already did. #88 returned the bare read failure, so a
  caller that stops or re-seals on that sentinel missed it.
- f2: a file carrier naming an empty file is refused with ErrFileCarrier
  ("delivered file is empty"), like a missing file. Codefly delivers by file
  only above resources.FileCarrierThreshold, so an empty file is a truncated
  write or a mount race. It used to read as absent, which let WorkspaceValue
  and ReadAuthority answer from the secret namespace (r144's "sec1" probe).
  The check sits in readFileCarrier, so every accessor and
  LoadEnvironmentVariables refuse it.
- f3: a name carried in both the public and the secret namespace is refused
  with the new ErrWorkspaceValueInBothNamespaces: two sources for one value,
  like inline plus file. It used to answer from the public one silently
  (r144's "pub" probe), so a stale public copy left behind after a move to
  the secret namespace kept answering. WorkspaceConfiguration and
  WorkspaceSecret, which each name one namespace, still answer.

Each test fails with its fix reverted. The README gains a Workspace values
section; AGENTS.md, the carrier skill and configuration-documents.md say an
empty delivered file is broken, not absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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