Skip to content

feat: Quick Support — one-time-code ad-hoc remote sessions (Milestone A) - #3153

Merged
ToddHebebrand merged 29 commits into
mainfrom
ToddHebebrand/quick-assist-feature
Aug 5, 2026
Merged

feat: Quick Support — one-time-code ad-hoc remote sessions (Milestone A)#3153
ToddHebebrand merged 29 commits into
mainfrom
ToddHebebrand/quick-assist-feature

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Quick Support — one-time-code ad-hoc remote sessions (Milestone A)

Implements the approved spec docs/superpowers/specs/2026-07-06-one-off-support-session-design.md
and plan docs/superpowers/plans/2026-07-06-quick-support-phase1.md.

A technician generates a short one-time code and sends it to someone whose machine
runs no Breeze agent — a customer's home PC, a prospect's laptop. The end user runs
a downloaded client (the existing Go agent in a new support mode) which enrolls an
ephemeral device into a hidden per-partner org. The entire existing remote-desktop
stack is then reused unchanged. Everything self-destructs at the end.

Milestone A only. Tier 2 (the elevated temporary service) is Task 14 and is not
in this PR.


Design decisions that deviate from the plan

1. /support/redeem does not return AGENT_ENROLLMENT_SECRET.
The plan said to return it if the installer flow already did. It does not — that
value appears nowhere outside config validation. Instead the single-use child
enrollment key carries its own key_secret_hash, which takes precedence over the
global secret in /agents/enroll. It is single-use, expires in 15 minutes, and
cannot enroll anything else, so the flow works with no new secret exposure.

2. The hidden org is unioned into accessibleOrgIds for partner-scope callers.
Without this, a partner user with orgAccess='selected' creates a session and then
reads back zero rows — the hidden org can never appear in the curated orgIds list
because it is absent from every picker, so RLS denies it silently.

Chosen over reading support sessions under a system context, which would have left
the connect path broken for the same users (POST /remote/sessions authorizes the
device through org scope). Special-casing remote-access authorization is a worse
place to carry the exception than one explicit org grant.

Accepted trade-off: the grant is partner-wide, so a technician can see and connect
to a colleague's Quick Support session. Per-creator scoping needs a bound connect
capability — a follow-up, not a silent default.

3. Cascade/export registration, absent from the plan entirely. support_sessions
is registered in CORE_ORG_CASCADE_DELETE_ORDER, DEVICE_DETACH_DEVICE_ID_TABLES
(its device_id is ON DELETE SET NULL — the audit row outlives the purged device),
CORE_DEVICE_ORG_DENORMALIZED_TABLES and CORE_TENANT_EXPORT_POLICY. The two new
columns are classified in the devices/enrollment_keys export policies, which the
column-level rule requires.

4. Migrations are dated 2026-08-13, not the plan's 2026-07-06, so they sort
after the existing last migration. The -a-/-b- split is load-bearing: Postgres
rejects any use of an enum value added in the current transaction (55P04), and
autoMigrate wraps each file in one transaction.


Bugs found that the plan did not anticipate

Consequence of the union in (2): the hidden org is visible to every org-scoped query,
so ~120 call sites needed explicit exclusion. The plan scoped this at three files.
Ordered by blast radius:

  • Automation triggers. An event raised by an ephemeral device carried the hidden
    org. The legacy branch of queueEventTriggers matches every automation with
    org_id IS NULL under that partner, and the config-policy branch then executes
    against payload.deviceId — the MSP's entire automation library running scripts on
    a stranger's home PC, reachable from any event the borrowed machine emitted. It
    bypasses resolveDeviceIdsForAssignment, so filtering the assignment resolver
    (what the plan described) would not have closed it.
  • Billing. countContractDevices feeds contract and invoice line quantities.
  • Paging. Every completed session would have raised a "device offline" alert.
  • Destructive workers. Reboot, patch, CIS hardening, backup, software
    remediation, and warranty sync (which ships serials to third-party vendor APIs).
  • Device conscription. snmpWorker, discoveryWorker and monitorWorker pick
    "any online device in the org" to run network probes — i.e. scan the end user's own
    home LAN.
  • Vulnerability correlation would have raised findings and critical alerts
    against a stranger's machine.

Deliberate non-exclusions

  • offlineDetector keeps processing ephemeral devices. The online → offline
    transition is the end-user-stop signal the reaper depends on; only the alerting
    is suppressed.
  • audit_chain anchoring/verification keeps covering the hidden org. Excluding
    it would create a tamper blind spot on the most sensitive session trail in the
    product. The cost is that a P1 divergence incident can name the hidden org —
    flagging explicitly for reviewer agreement.

Security posture

  • Code: 9 chars from a 30-symbol alphabet (no I/L/O/0/1), ~44 bits, SHA-256 stored,
    plaintext returned exactly once.
  • 15-minute redemption TTL, 8-hour hard cap, per-IP rate limits on every public route.
  • One atomic WHERE status='pending' claim makes a code strictly single-use.
  • Unknown, expired, claimed and malformed codes all return one identical 404.
  • Teardown order is load-bearing and pinned by test: deliver support_end while the
    socket is still authenticated → revoke all three token hashes and decommission →
    force-close the socket. The close is what stops a client whose command was lost
    from sitting connected until the hard cap.
  • support_end refuses to act unless the agent is genuinely in support mode. Its
    test wires in the real cleanup and asserts a seeded sentinel file survives, so a
    regression fails by deleting something rather than by a stubbed counter.
  • The served Windows binary is Authenticode-signed (Azure Trusted Signing, verified
    fail-closed through release-integrity-gate). The public page sets an honest
    expectation and tells the user to stop on an unknown publisher rather than
    coaching them past it.

Not verified

  • The Go client on real Windows hardware: self-delete trampoline, console-close
    SIGTERM, in-process capture under support mode.
  • Live end-to-end via worktree-stack.
  • Tier 2 (Task 14) is not implemented.

Verification

Every gate below was run, not assumed.

Gate Result
API unit 19,677 passing
API typecheck clean (needs --max-old-space-size; OOMs at the default)
RLS + chain integration 18/18 against real Postgres as breeze_app
Contract suites rls-coverage, tenantCascade (+execution), tenant-export-policy, export/erasure roundtrip
Web 4,089 passing · astro check 0 errors, 0 warnings
Shared 1,648 passing
Go agent 66 packages, go test -race, zero failures
Lint clean
Migrations apply clean on a fresh DB, no-op on re-apply, drift check green

The RLS suite is written so it cannot pass vacuously: the cross-tenant INSERT
test proves the byte-identical insert SUCCEEDS for the legitimate owner
immediately before asserting 42501 for the forger, so a policy that denied
everything would fail the pairing rather than sail through it.

🤖 Generated with Claude Code

Todd Hebebrand and others added 25 commits August 4, 2026 16:04
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Quick Support spec

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… notes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…spec

Adds review items 7-13: migration -a-/-b- split (55P04 enum trap),
end-path hardening, service-side Tier 2 consent guarantee, claimed-limbo
reaping, i18n mandate, Authenticode signing gate, milestone split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, quick_support org type

Adds the Quick Support data model (spec 2026-07-06-one-off-support-session-design.md):

- support_sessions (RLS Shape 1, direct org_id, four breeze_org_isolation
  policies in the same migration). code_hash is SHA-256; plaintext is shown
  once and never stored.
- organizations.type += 'quick_support' — the hidden per-partner org that
  holds ephemeral devices, one per partner via a partial unique index.
- devices.is_ephemeral, enrollment_keys.support_session_id.

The migration is split -a-/-b- because Postgres rejects any USE of an enum
value added in the current transaction (55P04) and autoMigrate wraps each
file in one transaction; the partial index on type = 'quick_support' must
therefore live in a later file.

Cascade/export registration (not in the original plan — added after checking
the contract tests): support_sessions goes in CORE_ORG_CASCADE_DELETE_ORDER,
DEVICE_DETACH_DEVICE_ID_TABLES (device_id is ON DELETE SET NULL so the audit
row outlives the purged device), CORE_DEVICE_ORG_DENORMALIZED_TABLES and
CORE_TENANT_EXPORT_POLICY. The two new columns are also classified in the
devices/enrollment_keys export policies, which the column-level rule requires.

Migrations are dated 2026-08-13 rather than the plan's 2026-07-06 so they
sort after the existing last migration (2026-08-12).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shared: SUPPORT_CODE_ALPHABET/LENGTH/PATTERN, normalizeSupportCode,
formatSupportCode, and the create/redeem zod schemas. The alphabet omits
I/L/O/0/1 because the code is read aloud as often as it is pasted.

API: generateSupportCode (randomInt rejection sampling, not randomBytes %
len, which would bias the first 16 symbols of a 30-symbol alphabet) and
hashSupportCode. Only the SHA-256 hash is ever stored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getOrCreateQuickSupportOrg lazily creates one hidden 'quick_support' org per
partner (plus the default site enrollment keys require), guarded by the
partial unique index.

Runs inside runOutsideDbContext + withSystemDbAccessContext because a
just-created org id is not yet in the caller's accessible_org_ids, so RLS
would reject the INSERT and its RETURNING under the request context.

The concurrent-create race is handled with onConflictDoNothing + re-select
rather than catching 23505: postgres.js rethrows errors handled inside
begin(), so transaction-abort recovery would surface as a 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST/GET /remote/support-sessions under the existing remote:access + MFA
gate. Create is partner-scope only: the hidden Quick Support org hangs off
the partner, and an org-scope token carries a partnerId but never passes
breeze_has_partner_access, so it would mint sessions it could not read back.
A system token with no partner context is rejected for the same reason.

'active' is derived at read time from live remote_sessions rows rather than
stored, so nothing has to hook the remote-session create/end paths to keep a
duplicate status in sync. The list endpoint batches the device-status and
live-session lookups across the page instead of per row (~100 round trips at
the default limit). codeHash is stripped from every response shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GET /support/check/:code returns nothing but a boolean, so it cannot be used
to enumerate tenants. POST /support/redeem performs one atomic
pending->claimed transition — the WHERE status='pending' guard is what makes
a code strictly single-use under concurrent redemption — and mints a
single-use child enrollment key.

Deviation from the plan, resolving its own open question: the plan proposed
returning AGENT_ENROLLMENT_SECRET alongside the child key, conditional on
the installer flow already doing so. It does not — the global secret appears
nowhere outside config validation. Instead the child key carries its OWN
per-key secret, which takes precedence over the global secret in
/agents/enroll. It is single-use, expires in 15 minutes, and cannot enroll
anything else, so no new exposure is created.

hashEnrollmentSecret moves to enrollmentKeySecurity.ts so the minting and
verifying sides cannot drift apart.

Unknown, expired, claimed and malformed codes all return one identical 404
body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ephemeral agent's WebSocket coming up is the only signal that the client
actually installed and reached us, so onOpen is what moves a claimed session
to 'ready' for the waiting technician.

Guarded on devices.is_ephemeral, which is already on the row onOpen just
loaded: this handler runs on every agent reconnect across a 10k-device
fleet, and normal devices must pay zero extra queries. The agent's context
org is the hidden Quick Support org that owns the row, so org RLS passes.

Gets its own try/catch rather than sharing the enclosing one, which would
file a failure under "failed to query device for online event" and
misdirect anyone debugging a session stuck at 'claimed'. A failure is
non-fatal — the reaper expires claimed-limbo sessions after 20 minutes.

Both branches are pinned by tests, including the non-ephemeral case
asserting zero support_sessions updates, so the perf guard is behaviour
rather than a review convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A key carrying supportSessionId enrolls an ephemeral device: the session
must still be 'claimed' and within its hard cap, the partner licence count
is skipped, and the session is bound to the new device id inside the same
transaction as the device insert.

Both licence counts (enrollment + provision) now exclude ephemeral devices,
so an ad-hoc support session can never consume a customer's seat. The
hostname-collision lookup excludes them too, so a repeat support run on the
same machine inserts a fresh row instead of taking the re-enrollment-token
branch.

The rejection reuses the expired-key response byte-for-byte, so a caller
holding only a key cannot probe session state; the real reason
(support_session_not_claimable) goes to the audit row only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GET /support/download/:platform streams the agent binary with the one-time
code in the filename, so the end user never types it. The binary is proxied
rather than redirected because a 302 to GitHub/S3 would name the file
breeze-agent-windows-amd64.exe and lose the code entirely. Cache-Control:
no-store — the code is in the filename.

A nonstandard port is encoded host_PORT, not host:PORT: ':' is illegal in a
Windows filename and Chromium silently rewrites it to '_' at save time,
which is how #2341 shipped silently-unenrolled installs. Matches the
existing windowsFilenameApiHost() convention the agent already decodes.

Also fixes this branch's typecheck and lint: zValidator now comes from
lib/validation (the repo's standardized wrapper) rather than
@hono/zod-validator, which no-restricted-imports rejects, and the
possibly-undefined destructures from Tasks 3-4 are narrowed explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ephemeral devices are exempt from offline ALERTING but deliberately not from
the offline status flip itself: going offline is how an ad-hoc support
session ends (the end user closed the client), and the reaper watches for
exactly that transition to tear the session down.

Without this, every completed Quick Support session would page the on-call
technician with a "device offline" alert. Both alerting paths are covered —
the immediate one in processMarkOffline and the config-policy re-evaluation
in processReevaluateOffline — and the re-eval sweep filters ephemeral rows
so the jobs are never queued. The re-eval handler keeps its own guard
because jobs queued before this deploy may still be in flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… revocation

endSupportSession is shared by the tech-initiated route and the reaper so
both revoke identically. Ordering is load-bearing and pinned by test:

  1. send support_end while the socket is still authenticated (cooperative
     path — the client deletes itself immediately)
  2. revoke all three token hashes and decommission the device
  3. force-close the socket

Step 3 is what stops a client whose support_end was lost from sitting
connected until the 8h hard cap: it is online, so its own offline dead-man
never fires. Closing forces a reconnect, the reconnect fails re-auth, and
the dead-man converges in <=10 minutes.

The audit records the disconnect result and command delivery rather than
collapsing them into success — a 'close-failed' socket plausibly stayed live
after revocation, which is exactly what an incident review needs to know.

No new guard was needed to stop a lingering client being reconnected to:
POST /remote/sessions already rejects any device not 'online', and teardown
sets 'decommissioned'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…elete path

The permanent-delete route's cascade moves to services/deviceDeletion.ts.
The Quick Support reaper purges ephemeral devices through the same function
rather than hand-rolling a second delete — two cascade implementations drift
the moment a table is added to one list and not the other, which is exactly
how this repo has produced FK-violation and orphaned-row bugs before.

The route keeps its own transaction and link-group dissolution; only the
row-removal sequence is shared. Behaviour is unchanged: 450 device route
tests still pass, and the ordering (transitive children, then detach
targets, then device_id cascade tables, then the device row) is preserved.

Also widens two hoisted test mocks whose inferred literal return types made
the failed-close cases untypeable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five passes, each the safety net for a specific failed teardown: lapsed
codes; claimed-limbo (client died between redeem and enroll, else the tech's
panel reads "Client connecting..." for 8h); hard cap; end-user stop detected
via the device going offline (there is no explicit stop API in v1); and
purging ephemeral device rows 6h after the session ends.

The purge re-reads the device and deletes only when is_ephemeral is true, so
a corrupted or mis-linked session row can never remove a real customer
device. Two tests pin that guard. Per-session failures are caught and
logged individually — a reaper that dies on one bad row stops protecting
every other tenant.

Termination goes through endSupportSession and purging through
deleteDeviceCascade, so there is exactly one revocation path and one delete
path in the codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`breeze-agent support` redeems a one-time code, enrolls into a TEMP
workspace (never C:\ProgramData\Breeze — the machine may already run a real
enrolled agent), runs in the foreground so desktop capture takes the
in-process path, and tears itself down on Ctrl+C, on support_end, or via a
10-minute dead-man switch.

The filename parser decodes host_PORT back to host:port, mirroring
installer_filename.go — ':' is illegal in a Windows filename and Chromium
rewrites it to '_' at save time. Browser duplicate-download suffixes are
tolerated in both the Chrome/Edge " (1)" and Firefox "(1)" forms.

support_end refuses to act unless the agent is actually in support mode.
That guard is what stops a forged or misrouted command from destroying a
permanently-installed agent, and its test wires in the REAL cleanup and
asserts a seeded sentinel file survives — so a regression fails by deleting
something rather than by a stubbed counter.

The Windows self-delete sets SysProcAttr.CmdLine verbatim rather than going
through exec.Command: Go's EscapeArg would emit backslash-escaped quotes
that cmd.exe cannot parse, and dropping the quotes breaks on any path
containing a space.

Not yet verified on Windows hardware: the self-delete trampoline, console-
close SIGTERM, and in-process capture under support mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Technician page: create dialog, one-time code shown large and copyable,
3s polling that stops permanently on terminal states (pinned by a test
asserting the poll count freezes), ConnectDesktopButton once the ephemeral
device is online, and End via runAction. The org picker is labelled
"Reporting only — this does not grant or change access to that customer's
data" so attribution cannot be mistaken for a tenancy control.

Public /quick page: unauthenticated, code entry with normalization, soft
validity check, Windows download, macOS marked coming soon. A dropped
connection renders as a distinct "check failed" state rather than "invalid
code" — telling someone their code is dead because the wifi blipped sends
them back to the technician for nothing.

The Windows publisher line sets an honest expectation and tells the user to
STOP if the publisher is unknown or unexpected, rather than coaching them
past the prompt. The signer name is isolated in one interpolated key,
defaulted to a neutral phrase, because the Azure Trusted Signing cert
profile display name is not in the repo and must not be guessed.

All 41 + 26 strings are literal-key t() across all seven locales with real
translations (fr-CA distinguished from fr-FR, es-419 formal usted).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A partner user with orgAccess='selected' had no way to reach their own
Quick Support sessions: the hidden 'quick_support' org is deliberately
absent from every org picker, so it can never appear in the curated
partnerUsers.orgIds list, and RLS then returned zero rows — silently. The
session created fine (creation writes under a system context) and the
technician's status panel simply stayed blank forever.

Both resolution paths are updated together. auth.ts and bearerTokenAuth.ts
must agree or session-JWT and OAuth/MCP callers behave differently for the
same user.

Chosen over the alternative of reading support sessions under a system
context: that would have left the CONNECT path broken for the same users,
since POST /remote/sessions authorizes the device through org scope, and
special-casing remote-access authorization is a worse place to carry the
exception than a single explicit org grant.

Accepted trade-off, deliberately: the grant is partner-wide, so a
technician can see and connect to Quick Support sessions raised by
colleagues at the same partner. Per-creator scoping would need a bound
connect capability and is a follow-up, not a silent default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…findings and abuse evidence

countContractDevices feeds contract line quantities AND invoice line
quantities, so an ephemeral device reaching it would bill a customer for a
machine that existed for twenty minutes and was never theirs.

Vulnerability correlation now bails at the org level for a quick_support
org: correlating a stranger's software inventory would raise findings — and
critical-detected alerts — against their home PC inside an org the MSP never
onboarded. Checked once per entry point rather than filtered into each join,
since a whole hidden org is never a legitimate correlation target.

The abuse-signal invariant counting live agents per partner excludes them
too; a burst of ad-hoc sessions should not read as agent sprawl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rker

These workers act ON devices. An ephemeral device is a stranger's personal
machine borrowed for one ~20-minute session, so reaching them means
rebooting a home PC, patching it, CIS-hardening it, backing it up, shipping
its serial to a warranty vendor, or paging an on-call technician.

The most serious hole was queueEventTriggers in automationWorker: events
raised by an ephemeral device carry the hidden org, the legacy branch
matches every automation with org_id NULL under that partner, and the
config-policy branch then executes against payload.deviceId — the MSP's
whole automation library running scripts on a stranger's machine, reachable
from any event the device emitted. It bypasses resolveDeviceIdsForAssignment
entirely, so filtering that function alone would not have closed it.

Three sweeps beyond the original inventory were found and closed the same
way: snmpWorker and discoveryWorker both pick "any online device in the org"
to run network scans (which would scan the end user's own LAN), and
userRiskJobs' org fan-out rested on an undocumented invariant rather than an
explicit filter.

offlineDetector is deliberately NOT filtered — ephemeral devices must keep
flowing through the offline transition because the end-user-stop detection
depends on it; only its alerting is suppressed.

audit_chain anchoring and verification are also deliberately left covering
the hidden org: excluding them would create a tamper blind spot on the most
sensitive session trail in the product. The cost is that a P1 divergence
incident can name the hidden org.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing surfaces

Completes the exclusion sweep. Because the hidden 'quick_support' org
deliberately stays inside accessibleOrgIds so RLS lets a technician reach
their own sessions, nothing filters it automatically — every enumeration,
count and aggregation needs an explicit predicate.

Covered here: device lists and status buckets, dashboard and SLA metrics,
enrollment/OS/agent-version analytics, every device report and the executive
summary, tag facets, dynamic group filter evaluation and its preview count,
update-ring targeting, vulnerability and patch and policy compliance
denominators, security-posture coverage, software inventory (raw-SQL
aggregates, filtered via the shared conditions both interpolate), org lists
and pickers, the sites list and its device counts, AI tools, MCP resources,
the mobile app, the customer portal, and the public Partner API.

Deliberately skipped, each verified by reading the code rather than the line
number: by-id device/org resolves (group membership validation is already
gated on device.orgId !== group.orgId), the slug-uniqueness read (the hidden
org's slug must stay in the taken-set), and tenant offboarding/erasure (the
hidden org must be drained with its partner).

Also repairs a regression from the enrollment-secret extraction: a second
test file mocks enrollmentKeySecurity and lacked the moved export, which
silently broke four per-key-secret assertions including two that verify a
mismatched secret is REJECTED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two suites against real Postgres as the unprivileged breeze_app role — the
only place the tenancy contract is actually proven, since the unit suites
mock ../db wholesale and never evaluate a policy.

RLS suite guards against vacuity deliberately: the cross-tenant INSERT test
proves the byte-identical insert SUCCEEDS for the legitimate owner
immediately before asserting 42501 for the forger, so a policy that denied
everything would fail the pairing rather than pass it.

Chain suite covers provisioning idempotency, redeem, double-redeem
rejection (the atomic single-use claim), ephemeral enrollment and session
linkage, the licence-cap exemption paired with a normal enrollment still
being refused, token revocation, and the reaper purging the device while the
session row survives with device_id NULL.

Two fixes while getting them green:
- Teardown deletes each organization in its own transaction. The org-delete
  trigger takes a PARTNER export lock, and the lock hierarchy forbids
  acquiring one after an organization lock is already held in the same
  transaction.
- /agents/enroll answers 201, not 200 — the assertion was wrong, the route
  was right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers the technician flow, the partner-scope requirement, and the
partner-wide visibility of sessions.

Documents what the end user's machine actually gets, since that is the
question a customer will ask: user-session only, no service, self-deleting,
and an ephemeral device that never counts toward licensing, never appears in
lists, reports or invoices, and is never patched, rebooted, backed up or
alerted on.

States the Phase 1 limits plainly rather than leaving them to be discovered
— Windows only, and no elevated mode yet, so the client cannot drive UAC
prompts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This repo is public and the plan carried a Tailscale address for the Windows
test VM. Points at the internal note instead.

Pre-existing: the same address appears in five other plan docs already on
main (installer-enrollment, security-auth, remote-desktop, pam). Not
in scope to fix here, but they are exposed and worth a separate scrub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: ccfcd04
Status: ✅  Deploy successful!
Preview URL: https://f5162991.breeze-9te.pages.dev
Branch Preview URL: https://toddhebebrand-quick-assist-f.breeze-9te.pages.dev

View logs

Todd Hebebrand and others added 4 commits August 4, 2026 22:33
The struct-field alignment in this block was already unformatted on main,
but adding IsConnected() puts the hunk inside this branch's diff, and the
Lint Agent job runs golangci-lint with --new-from-rev — so it lands as a new
issue and fails CI.

Alignment only; no behaviour change. go vet is clean across every package
this branch touches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
staticcheck ST1005 — error strings must not be capitalized or end with
punctuation. The sentinel was carrying the end-user sentence verbatim.

Splits the two concerns rather than just deleting the period: the sentinel
is now terse and lowercase, and the friendly wording moved to the display
site in agentapp, which is the layer that actually knows its audience is a
non-technical stranger. The message the end user sees is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The site-scope detector flagged both GET handlers as new offenders because
they read `devices`. Exempting rather than gating, with the reasoning
recorded so a future reviewer can re-derive it:

- Neither route takes a device id from the caller. The ids come from
  support_sessions rows RLS has already authorized.
- The only device datum returned is the boolean `deviceOnline`; no
  per-device rows are disclosed.
- The devices are ephemeral rows in the hidden per-partner 'quick_support'
  org, which is granted to PARTNER scope only, while `allowedSiteIds` is an
  org-scope-only axis. A site-restricted caller cannot see these sessions at
  all, so there is nothing to reach a device through.

Deliberately not added to SITE_SCOPE_INPUT_BASELINE — that set is frozen and
only shrinks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssist-feature

# Conflicts:
#	apps/api/src/index.ts
@ToddHebebrand
ToddHebebrand merged commit 75c5b28 into main Aug 5, 2026
58 checks passed
@ToddHebebrand
ToddHebebrand deleted the ToddHebebrand/quick-assist-feature branch August 5, 2026 21:43
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