Skip to content

update windows signing - #14

Merged
digiogithub merged 8 commits into
mainfrom
dev
Sep 7, 2026
Merged

update windows signing#14
digiogithub merged 8 commits into
mainfrom
dev

Conversation

@digiogithub

Copy link
Copy Markdown
Owner

No description provided.

digiogithub and others added 8 commits September 4, 2026 23:02
…abilities

Two new extension capabilities, both generic and hook-shaped, following
the ConfigOverlayProvider pattern: declare in pkg/extension, discover
with Capability[T], adapt with panic containment in internal/extensions.

IdentityProvider lets an extension answer who the current user is. The
adapter bounds the call at two seconds because it sits on the
synchronous knowledge-base write path, and the result is read per use,
never cached. It fills the Attribution.UserID field that already existed
and was never set, and adds user_id to the session index metadata. Email,
groups and device id deliberately do not widen Attribution: attribution
is stamped on every event that leaves the machine, so carrying them
there would export personal data on every write with no separate
consent.

ProviderRequestDecorator lets an extension add per-request headers to an
LLM provider call. Static per-account extraHeaders already covered the
fixed case, so this hook exists only for values that change per call,
such as a session id. It is installed as client middleware in the OpenAI
and Anthropic constructors, and the adapter refuses to overwrite
Authorization, x-api-key and the anthropic-* headers rather than
trusting the extension.

Also closes two gaps in the config work: every configuration mutator is
now covered by the locked-key guard, with an AST-based test over all 53
of them so a new mutator that bypasses updateCfgFile fails the suite,
the remaining API write paths return 409 config_key_locked, the settings
UI marks locked fields as managed instead of only refusing the write,
and overlay_applied now also fires when only the lock list moves.

Verified: go build ./..., go vet ./... and go test on config, extension,
extensions, llm, api, tui and app, with -race on the five core packages.
The four internal/llm/agent failures are pre-existing on HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqTkMtjq3BciZXamva2FgQ
Third UI-facing hook after the config overlay and the locked keys.
UIPolicyProvider lets an extension declare which settings sections are
hidden, which are read-only, the label to show on them and an operator
banner. Providers are merged by unioning the two sets, with the label
and the banner taken from the first provider in load order that sets
one; hidden beats read-only.

Consumed on all three surfaces: the TUI settings page next to the
locked-key marker that landed with CORE-2, the web settings store
through a new GET /api/v1/config/ui-policy mirroring the locked-keys
endpoint, and desktop preferences, which are the same web bundle under
Wails.

Hiding is never the only defence. A new nil-by-default
SetRestrictedPathSource hook joins hidden and read-only paths to the
locked-key check in the config write path, so a hidden section is
refused on write even if a client asks anyway. The adapter memoises the
merged policy for one second so the per-changed-path check does not fan
out to extensions on every write.

Verified: gofmt on every changed file, go build ./..., go vet ./... and
go test on extension, extensions, api, tui, config and app, with -race
on the extension packages, plus the web typecheck and lint. The four
internal/llm/agent failures are pre-existing on HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqTkMtjq3BciZXamva2FgQ
Two hooks, both generic.

Extensions can now subscribe to tool, mcp, skill and provider events
alongside the config topic that already existed, through the
EventSubscriber interface they already implement. Publish points sit
where the event actually happens rather than in one convenient place,
and publishing never blocks or fails the operation that triggered it,
following the config bus precedent of dropping on a full channel. The
payloads carry names, durations and outcomes only: no arguments, no
results, no instructions and no credentials ever leave through them.

HostServices.Prompts lets an extension run a one-shot prompt, which is
the contract the core already offers people through pando -p, expressed
in strings and numbers. Any automation extension needs those three
lines, and this way the core exposes no sessions, no models and no tool
loop to get them.

A second candidate hook is refused in ADR 005: a serve flag carrying a
job file would be a pass-through whose format exactly one extension can
parse, and the core already decided the opposite way by mounting
extension commands under a namespace so nothing can shadow a core
command. The cost of saying no is one longer command line.

Verified: gofmt on every changed file, go build ./..., go vet ./... and
go test ./..., which still shows only the four internal/llm/agent
failures that were already present on HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqTkMtjq3BciZXamva2FgQ
…on system does

Every camelCase option an extension asked for was unreachable. Viper
folds keys on the way in and the host services looked them up exactly,
so an extension asking for baseURL could never find the value a user
wrote as baseURL. The doc comment already promised case-insensitive
keys throughout; the code did not deliver it.

The effect was total rather than cosmetic: the first enterprise
extension refused to provision, and because six others declare it as a
dependency, none of them loaded at all. Managed mode, private mode and
memory sync could not be exercised end to end because of this.

Every accessor now resolves through one lookup that folds the requested
key the same way the stored one was folded, and the entry's own config
table is folded at its top level so reading the raw map agrees too.
Nested tables are left alone: those are the extension's data, not its
option names. Two spellings of one option no longer shadow each other
in silence; the first wins deterministically and the extension's logger
says which was kept and which was ignored.

The accessor surface was only Bool, String and Int, which is why an
out-of-tree extension had hand-rolled the rest over the raw map.
Duration, StringSlice, Float64 and Map are now provided and coerce
across TOML integers, JSON floats and environment strings.

An extension option can now also come from the environment, which a
container deployment needs and did not have. One honest gap remains:
whether an extension is enabled is a field of the entry rather than an
option, and it still needs a file or an overlay.

Verified: gofmt, go build ./..., go vet ./..., the targeted tests with
-race, and a full test run showing only the four internal/llm/agent
failures that were already on HEAD. The regression test fails on the
old code. Proved on the real case too: a composed enterprise binary
built against this core loads the extension that used to refuse, and
its six dependants with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqTkMtjq3BciZXamva2FgQ
The token endpoint was exempt from the token check, so anything that
could reach a running server could ask it for its own token. That was
already wrong; it became reachable in practice now that a control plane
drives remote instances over this surface.

The exemption survives only where it is defensible: a loopback bind, or
a request that is already authenticated by basic auth. An exposed
server configured with no basic auth user is refused with a message
naming what is missing, rather than quietly handing the token over, and
a caller that already holds one can still refresh it.

Verified: go build ./..., go vet ./... and the api package tests, with
no new failure against the four that were already failing on HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqTkMtjq3BciZXamva2FgQ
The read-only configuration view an extension holds resolves a dotted
path against the JSON encoding of the configuration struct, which is
not the spelling anyone actually writes. A configuration file, a pushed
overlay and a lock list all name a setting the way the documentation
does, so a path that named a real setting could come back absent.

That was not a cosmetic mismatch. An enterprise extension's drift
detection asked for a value, was told it did not exist, and reported a
converged installation as diverged; the divergence asked for a reload,
the reload republished the applied event, the subscriber checked drift
again, and the loop ran at the reload debounce. Measured downstream:
two hundred and twenty three identical events in sixty seconds, and
every device permanently reporting drift while it was exactly
converged.

A segment now falls back to a case-insensitive match, with an exact
match still preferred, so a configuration that genuinely carries two
keys differing only in case is unaffected.

This is the same class of defect as the extension option lookup fixed
earlier today, in the second of the two places that spell a
configuration key.

Verified: go build ./..., go vet ./... and the extension packages, with
a test that fails on the previous code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqTkMtjq3BciZXamva2FgQ
The Windows CLI shipped unsigned. Signing needs signtool and the Trusted
Signing dlib, neither of which has a Linux build, so the exe cross-compiled
on the Linux runner now travels through an artifact to a Windows job that
signs it and repacks the zip.

Authentication is OIDC: the windows-sign job runs in the release environment
and mints a token that a federated credential on the pando-github-trusted-signing
Entra app accepts, so no signing secret is stored in the repository. The
endpoint, account, certificate profile and identity ids come from repository
variables.

The unsigned zip is uploaded as unsigned-windows, deliberately outside the
dist-* pattern the release job globs, so a release cannot ship it by accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXcWfFXuK9aWAg8uvMfBgj
@digiogithub
digiogithub merged commit f44c2e0 into main Sep 7, 2026
6 checks passed
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