A reporter on #1635 found that Assignment required alone does not stop
Entra emitting every security group when the app manifest still lists
SecurityGroup in groupMembershipClaims. Add the manifest check to the
group-overage warning.
Refs #1635
The mock:on and mock:off scripts used GNU 'sed -i' syntax, which fails on
the default macOS BSD sed. The '||' fallback then appended a fresh
PULSE_MOCK_MODE line on every invocation, so a local .env accumulated three
contradictory entries.
They also targeted the repo-root .env, which hot-dev.sh does not consult
when choosing the data directory. hot-dev.sh reads the canonical flag from
tmp/dev-config/.env, written authoritatively by toggle-mock.sh. The npm
wrappers were therefore a no-op that corrupted .env as a side effect, while
leaving the operator believing mock mode had switched.
Point them at toggle-mock.sh, which handles BSD sed, writes the canonical
file, syncs the runtime env and restarts the managed runtime. Add
mock:status and mock:edit; the hot-dev startup banner already advertised
'npm run mock:edit', which did not exist.
Extend the deployment-installability contract to cover the mock wrappers
alongside the existing repo-root dev entry rules, and add a regression test
asserting the wrappers delegate rather than rewriting the flag inline, and
that every mock command named in the hot-dev banner exists.
The three deep-dive documents describe enumerations and tuned constants that
live in code. Prose cannot notice when a fifth state or an eighth signal type
is added next to it, so the documents would have rotted silently.
These guards derive the truth from the source rather than asserting literal
strings. A test that only checks the document contains RESOLVING keeps
passing when a new state appears, which is the drift actually worth catching.
Covered, in both directions where the set is enumerable. Session states and
tool kinds against internal/ai/chat/fsm.go and the ToolCallKind String
method. Patrol signal types and the five tuned signal thresholds against
internal/ai/patrol_signals.go. The investigation attempt limit and cooldown
against internal/ai/findings.go. The look-before-asking bound and the tool
concurrency cap against internal/ai/chat/agentic.go. Identifiers named in the
structured errors section against the declared error codes and tool names.
Error codes are checked document to code only. The document names a
representative sample rather than all 45 declared codes, so requiring the
reverse would force a table that churns on every addition while catching
nothing a reader cares about. A rename or removal still fails.
Writing the guard immediately found a real mistake in the document it
guards. ErrCodeFSMBlocked is FSM_BLOCKED, not fsm_blocked as the prose
claimed, corrected here.
Each guard was verified to fail by adding a fifth session state, adding a
signal type, and changing a threshold, then confirming the failure named the
document and the value.
Contract-Neutral: test and documentation only
AI.md, AI_AUTONOMY.md and PULSE_PRO.md linked nine times into
docs/architecture/, which .gitignore marks as not for public release. The
targets were never missing, they were deliberately unpublished, so every one
of those links was dead for anybody but the maintainer.
Write the three promised documents against the code and publish them under
docs/ where the rest of the shipped set lives.
PATROL_ARCHITECTURE.md covers a run end to end. The interesting part is that
deterministic signal detection runs after the model, not before, so unmatched
signals catch what the model failed to file. Signal types, the thresholds
those signals derive from your own alert settings, and every condition in
Finding.ShouldInvestigate are documented from internal/ai/patrol_signals.go
and internal/ai/findings.go.
ASSISTANT_SAFETY.md documents the session state machine in
internal/ai/chat/fsm.go, its four states, the tool classification it runs on,
and its invariants. No write without a validated target, no second write
before the first is verified, no final answer about an unverified change, and
no attempt count that wears the gate down.
ASSISTANT_ARCHITECTURE.md covers the agentic loop around that machine, the
three-phase pipeline, why only execution parallelises and at what cap, the
read-before-write batch that must stay ordered, the look-before-asking gate
and its bound, and the stable error codes.
Also removed two older pointers into the same private directory, in API.md
and UPGRADE_v6.md, and the two references to ENTITLEMENT_MATRIX.md in
PULSE_PRO.md. That file exists locally and is a monetization document, so
publishing it is not a documentation decision.
Unresolvable intra-doc links are now 2 of 218, both internal release-control
documents deliberately withheld from the shipped set.
Contract-Neutral: documentation only
This document is linked from Settings, API Access, Agent integrations, so
its reader is somebody trying to connect an MCP client to their own
instance. It was written as a note to the maintainer, opening by describing
what landed on a branch and offering itself as a reminder in three weeks.
Rewritten for that reader. The endpoint reference, error codes and adapter
details are unchanged in substance, since an integrator needs all of them.
What changed is who the prose is addressed to.
Three things were cut rather than reworded because they were about building
Pulse rather than using it.
The Provable claims section listed test function names, referenced numbered
work slices, and recounted that discovery used to return 401 before a fix.
The guarantees underneath were worth keeping, so they are stated as
guarantees now, that the manifest matches the implementation, that
discovery needs no token, that error codes come in two layers, and that the
surfaces are exercised together. The test names that enforce them are not
the reader's concern.
Where to read more pointed at internal subsystem contracts under
docs/release-control, which are deliberately not shipped, and at Go and TSX
source paths. It now points at Configuration, API and AI, which are all
shipped and all resolve in the viewer.
The roadmap sentence about the next work item being whatever friction first
usage surfaces became an invitation to open an issue. The Gatekeeper
warning stayed, because that one is something a reader actually hits.
Title is now Pulse agent integrations, matching the panel the reader
arrives from, rather than substrate, which is not a word they saw.
Contract-Neutral: documentation rewrite, no contract delta
The second thing a user read in the documentation portal was the v6
Execution Canonical Source section, telling them to consult
release-control internal files, run status_audit.py, registry_audit.py and
contract_audit.py, and observe pre-commit partial-staging rules for
.husky/pre-commit and canonical-governance.yml. That is agent and
maintainer execution guidance, and it means nothing to somebody who has
just installed Pulse. The docs are shipped and served raw under /docs, so
it reached self-hosted operators as written.
Move it into SOURCE_OF_TRUTH.md as an Execution Entry Point subsection,
which is where the section already pointed and which is excluded from the
shipped docs set. Nothing is lost: the three facts that lived only in the
index, contract_audit.py, the hook-sensitive partial-staging list, and the
substantive-contract-section rule, are all carried across, and every other
reference already existed under Canonical Control Files.
The operator index now runs from its welcome line straight into Getting
Started.
The evaluation mode added earlier today was hollow. An unlicensed control
plane starts, but release-build client runtimes only trust entitlement
leases chained to a Pulse-signed licence, so its client workspaces ran
without the capabilities the provider was evaluating. Standing the stack
up proved the portal and the isolation boundary and nothing else.
setup.sh now requests a capped evaluation licence from the licence server
when no licence path is set, sending only the public half of the key it
generated locally. It degrades rather than blocks: a missing key, an
unreachable server, or a licence-free response leaves the install
unlicensed with an explicit warning, an existing licence on disk is
reused, and PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE skips it for air-gapped
hosts. Guarded with an if-test rather than a trailing true inside the
command substitution, because the derive helper calls die and exit in a
subshell is not a status that can be caught, so setup.sh aborted under
set -e.
Second fix. The lease capability ceiling was selected by licence
presence, so an unlicensed provider control plane fell through to the
Pulse-hosted branch and minted leases claiming relay, mobile and push,
which a provider deployment cannot serve and which previously caused
repeating relay registration failures in client runtimes. The ceiling now
follows hosting via SetProviderHosted. providerChained keeps its narrower
meaning of having a licence available to embed.
Third. Corrects a sentence I wrote into cloud-paid.md this afternoon
claiming msp_eval carries the same MSP capabilities. A plan version
selects the workspace cap; it does not entitle a workspace.
The regression test was negative-tested by reverting the ceiling selector
and confirming it catches relay. All three setup.sh degradation paths
were exercised directly. Licence server side is pulse-pro 7f6a319 and is
not live until the next deploy-license-server run.
Two mandatory round-trips stood between an interested MSP and their first
screen, and neither was technical.
setup.sh required four image digests shipped as literal <pin>
placeholders, so the only way to get them was to ask. All four images are
publicly readable, so there was never anything to hand out. setup.sh now
resolves each blank pin to an immutable digest from its published tag via
buildx imagetools and writes it back to .env; hand-set values are left
alone.
setup.sh then died outright without a licence file, so nobody could start
the stack, create a workspace, or see the portal until a human minted a
licence for them. The control plane already ran unlicensed via
ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence
path that is set but missing is still a hard failure, since that is a
misconfiguration rather than a choice.
Unlicensed now means evaluation rather than the cheapest paid tier. The
env fallback defaulted to msp_starter, handing every unlicensed
deployment the full 5-client Starter allowance and leaving no boundary
between evaluating and buying. Adds msp_eval at 2 workspaces: same
capabilities, smaller cap, not purchasable, not on the public ladder.
An isolation guarantee is the one claim a provider cannot evaluate from a
screenshot, and both MSP leads this year went quiet at exactly this step.
Contracts: cloud-paid records the unlicensed plan rule and the
strictly-below-paid invariant; deployment-installability records
credential-free, correspondence-free installability.
Verification: TestMSPEvalCapStaysBelowCheapestPaidTier,
TestCanonicalizePlanVersion_MSPEval,
TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was
negative-tested by reintroducing a <pin> placeholder and confirming it
fails. ensure_image_pins exercised against the live registries.
licensing, cloudcp, control-plane and installtests all green.
The resource_metadata table (canonical_id-keyed custom_url/custom_name/
notes/tags) had no readers or writers anywhere in pulse or
pulse-enterprise; guest metadata is owned by the JSON-file store in
internal/config/guest_metadata.go keyed by instance:node:vmid. Stop
declaring the dead table and its index in fresh schemas. Deployed
databases are left alone deliberately — no DROP TABLE migration, since
a stale unused table is harmless. store_test.go now fails if a fresh
store's sqlite_master reintroduces resource_metadata.
The findings runtime keys resources by whatever ID the producer used:
unified-derived findings carry the hashed canonical resource ID, but
Patrol guest inventory rows carry the node-scoped Proxmox source ID
(instance:node:vmid). The operator-state provider closure looked the
reference up in the unified store directly, which keys by canonical ID
only, so maintenance windows, intentionally-offline intent, and
criticality silently never reached guest findings - Patrol could
auto-propose fixes for a guest the operator had flagged as in
maintenance, and the suppression auto-ack never fired for guests.
A reference that misses the store now resolves through the registry
(GetByReference, which after the node-independent guest identity change
also covers retired canonical-ID eras and node-scoped guest source
references) and retries under the canonical ID. Direct canonical hits
stay as cheap as before; the resolution hop only runs on a miss.
Contract-Neutral: operator-state provider ref-resolution fix: router.go closure bug fix with api-contracts delta staged; dependent subsystem contracts (agent-lifecycle, performance-and-scalability, security-privacy, storage-recovery) have no behavioral delta
A guest's unified canonical ID hashed its node-scoped source ID
(instance:node:vmid), so a live migration to another cluster node
re-minted the resource and orphaned every operator-owned row keyed by
the old ID: explicit availability check links (fail-closed by design,
the reported symptom in #1669), alert overrides, operator state, action
audits, manual links, and recovery subjects. VMIDs are unique within a
cluster, so guests now derive their canonical ID from instance+VMID
("proxmox-guest:<instance>:<vmid>") and keep it across migrations. The
guest-metadata half of #1669 was fixed separately at the metadata-store
layer.
Existing installs converge through record-declared succession: ingest
declares the retired node-scoped IDs superseded for every node the
instance currently knows (current names plus native aliases), so rows
orphaned by pre-upgrade migrations also re-key. Successions are now
recorded durably in a canonical_id_successions table, which memoizes
the re-key (steady-state rebuilds re-declare the same eras every tick
without touching SQL) and lets change-journal reads merge retired guest
eras the way pin EraIDs do for hosts. The succession re-key also covers
manual link/exclusion rows.
Availability links resolve retired canonical IDs and old-node source
triples through a registry superseded index plus guest-triple parsing
(persistence keys only, ambiguity fails closed), and the stored
LinkedResourceID re-homes to the current canonical ID on the
alert-migration cadence. Recovery subjects converge on the same
derivation via CanonicalSubjectResourceID, the mapper's registry-miss
fallback, node-independent external guest keys, and the store's startup
backfill, which also sweeps posture rows stranded under retired subject
keys. Metrics history and frontend row identity key off the node-scoped
source ID and are deliberately unchanged.
Public declaration of how AI tools are used to build and maintain
Pulse, linked from the README documentation list and the FAQ.
Automated triage replies now carry a disclosure footer per the
policy this document sets.