Skip to content

feat(cli): add feedback add and delete commands for quick CLI feedback - #5988

Open
kanadgupta wants to merge 40 commits into
developfrom
kanad-claude-2026-07-22/feedback-command
Open

feat(cli): add feedback add and delete commands for quick CLI feedback#5988
kanadgupta wants to merge 40 commits into
developfrom
kanad-claude-2026-07-22/feedback-command

Conversation

@kanadgupta

@kanadgupta kanadgupta commented Jul 29, 2026

Copy link
Copy Markdown
Member

What

Adds a TS-only supabase feedback command family (from the original brainstorm) to the legacy shell so users — and agents — can send quick, low-friction feedback to the Supabase team without filing a GitHub issue, and revoke a submission later (e.g. an accidentally pasted secret):

supabase feedback add "when I run multiple stacks in parallel I get port conflicts"
# → Thanks for the feedback!
# → To delete this feedback later, run: supabase feedback delete <token>

supabase feedback delete 123e4567-e89b-12d3-a456-426614174000

Part of CLI-1946; the delete command is CLI-2188. Scope evolved in this thread: feedback add (no btw alias) plus a token-based delete path, rather than full user-scoped CRUD.

How feedback add works

  • Message resolution: positional args → piped stdin (non-TTY) → interactive prompt (TTY, text mode) → error. Messages starting with a dash use the -- sentinel.
  • Transport: submits through the SECURITY DEFINER RPC submit_interfaces_feedback (feat: table for collecting interfaces feedback supabase#48420) via supabase-js — the table has no insert grant, so the RPC is the only door and the delete token is always server-generated. The committed key is a publishable (anon) key, safe to ship in the binary. 10s timeout.
  • Delete token: the RPC returns a uuid delete_token exactly once. Text mode prints it with a "to delete this later" hint; json/stream-json carry it as delete_token in the result payload. The CLI never persists it.
  • Submission context: CLI version, user agent, OS/arch, agent detection (is_agent/agent_name via @vercel/detect-agent, to support the activation analysis in AI-961), and the linked project ref. metadata.source: "cli" distinguishes CLI rows from the future MCP path. The access token is never sent; user_id is never sent.
  • Project ref resolution: SUPABASE_PROJECT_ID<workdir>/supabase/.temp/project-ref (the file supabase link writes) → omitted. Reads the file directly (not via LegacyProjectRefResolver, whose prompt path needs the platform API) so feedback works logged-out; a broken ref file degrades to "unlinked".
  • Environments: the feedback backend follows the resolved profile the same way the Management API URL does (staging profiles → staging project). Production intentionally reuses the staging project until a dedicated one is provisioned (tracked in CLI-1998).

How feedback delete <token> works

  • Validation: the token must be a UUID (checked client-side to avoid PostgREST's cryptic uuid-cast error) and is lowercased before sending.
  • Preview first: a token-scoped read shows the feedback text before anything is deleted, so the user can verify what the token unlocks. Zero rows → a friendly not-found error covering all three indistinguishable causes (wrong token, already deleted, project-ref context mismatch).
  • Confirmation: interactive text mode prompts (Permanently delete this feedback? [y/N]); --yes/SUPABASE_YES skips it. Machine modes (json/stream-json) fail loudly without --yes rather than deleting silently — same contract as logout.
  • Deletion: a hard DELETE with Prefer: count=exact; the CLI verifies Content-Range reports exactly one row. Authorization is the x-feedback-token request header matched by RLS — the delete_token=eq. URL filter only satisfies PostgREST's filterless-delete rejection.
  • Context gate: rows submitted from a linked project also require the matching x-feedback-project-ref header. The delete command resolves the ref as --project-refSUPABASE_PROJECT_ID → linked-ref file and always sends whatever resolves (extra context against a context-free row is ignored server-side).
  • Machine modes return the deleted text in the result payload: { "feedback": "...", "message": "Feedback deleted." }.

Privacy note for reviewers

The feedback message, the delete token, and the --project-ref value go only to the feedback backend — never to PostHog. Message and token are positional arguments, which extractChangedFlagNames structurally excludes from the flags telemetry property; --project-ref is recorded by name only with its value redacted. Regression tests assert none of them appear in captured analytics events.

Reviewer-relevant context

  • The shared service was reshaped from FeedbackSubmitter (insert-only) into FeedbackClient (submit/preview/delete) in src/shared/feedback/feedback-client.{service,layer}.ts, and the profile→environment mapping and cli-config layer wiring were hoisted to the feedback family root (feedback.layers.ts, feedback-project-ref.ts) now that two commands share them.
  • src/shared/feedback/database.types.ts is generated (supabase gen types) and excluded from formatting/knip.
  • The e2e golden path is one combined add → delete round trip against the staging project (pinned --profile supabase-staging), which also cleans up its own row each run.
  • postgrest-js silently retries idempotent GETs (the preview) up to 3× with backoff on network errors; mutations and the RPC don't retry. It settles fine — noted because supabase-js exposes no way to disable it.
  • The merge from develop picked up the CLI-1970 docs restructure: the feedback commands are recorded in docs/go-cli-divergences.md (TS-only section) and registered in legacy-docs-spec.tables.ts (other-commands tag) instead of the old porting-status tracker.
  • Heads-up on LegacyCliConfig.projectId: it is a bare SUPABASE_PROJECT_ID env passthrough — it does not read config.toml or the linked-project file, so it is None in a linked project unless that env var is set. An earlier revision of this branch used it directly as "the linked project ref", which meant project_ref was always null in practice. The AGENTS.md row that described it as resolving project-id from config.toml is corrected here, since that phrasing is what made the field look project-aware.
  • services.integration.test.ts now uses an isolated temp workdir instead of process.cwd(), fixing machine-dependent behavior when the developer has local supabase start state.

🤖 Generated with Claude Code

kanadgupta and others added 10 commits July 28, 2026 08:17
The vendored effect clone in .repos/ drowns out workspace results in
editor-wide search.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LegacyCliConfig.projectId is a bare SUPABASE_PROJECT_ID env passthrough, so
the feedback submission's project_ref was null in a linked project unless
that env var happened to be set. Fall back to <workdir>/supabase/.temp/
project-ref, the file supabase link writes, mirroring the soft-load half of
LegacyProjectRefResolver.resolveOptional. The file is read directly rather
than through the resolver so the command keeps working unauthenticated; a
broken ref file degrades to unlinked instead of failing the submission.

The previous integration test injected projectId straight into the config
mock, so it only proved the handler forwarded the field and never exercised
resolution -- despite being named for the workdir-linked scenario that did
not work. Replace it with coverage that seeds the real file, plus env
precedence, unlinked, and unreadable-file cases.

Also correct the AGENTS.md row claiming LegacyCliConfig reads project-id
from config.toml, which is what made this field look project-aware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanadgupta
kanadgupta marked this pull request as ready for review July 29, 2026 05:25
@kanadgupta
kanadgupta requested a review from a team as a code owner July 29, 2026 05:25
@kanadgupta
kanadgupta requested review from gregnr and mattrossman July 29, 2026 05:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 656f13a667

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/feedback/feedback.e2e.test.ts Outdated
Comment thread apps/cli/src/legacy/commands/feedback/add/add.handler.ts
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase/cli/supabase@3c41e19d542545464ef37d480e885913b824ed29

Preview package for commit 3c41e19.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 830e565f2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/feedback/add/add.handler.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae5d202bd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/feedback/feedback.handler.ts Outdated
Comment thread apps/cli/src/shared/feedback/feedback-submitter.layer.ts Outdated
Comment thread apps/cli/src/legacy/commands/feedback/add/add.handler.ts Outdated
…alias

Restructures the TS-only feedback command from a single `supabase feedback`
command (with a `btw` alias) into a `feedback` group with an `add`
subcommand, following the nested-subcommand layout. Telemetry now records
`command: "feedback add"`; behavior is otherwise unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanadgupta kanadgupta changed the title feat(cli): add feedback command for quick CLI feedback submission feat(cli): add feedback add command for quick CLI feedback submission Aug 13, 2026
…07-22/feedback-command

# Conflicts:
#	apps/cli/src/legacy/commands/functions/delete/delete.integration.test.ts
#	apps/cli/src/legacy/commands/functions/download/download.integration.test.ts
#	apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ca265f84d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/shared/feedback/feedback-submitter.layer.ts Outdated
@kanadgupta
kanadgupta marked this pull request as draft August 13, 2026 18:09
kanadgupta and others added 8 commits August 14, 2026 15:44
Applies the accepted Codex review findings on the feedback family:

- Abort in-flight feedback requests on fiber interruption: the client's
  run helper now threads Effect.tryPromise's interruption signal into
  every postgrest call via AbortSignal.any with the 10s timeout, so
  Ctrl-C can no longer let a submit commit after cancellation.
- Gate the interactive prompt on stdin.isTTY in addition to
  output.interactive, so whitespace-only piped stdin with a TTY stdout
  fails with the documented empty-message error instead of opening a
  prompt against exhausted stdin.
- Honor the --agent yes|no override in the submission payload via
  legacyResolveAgentMode (hoisted from db query to legacy/shared);
  --agent no also suppresses the detected agent_name.
- Refresh ~/.supabase/telemetry.json on every feedback add/delete run
  via the standard Effect.ensuring(telemetryState.flush) finalizer.
- Honor -o json on both commands (machine payload via encodeGoJson,
  stdout payload-only); values outside feedback's pretty|json enum are
  rejected pre-run like db query's restricted set. yaml/toml stay
  unsupported: the struct-spec encoders reproduce Go field names and no
  Go struct exists for this TS-only command.
- Route feedback HTTP through the legacy transport: a composed fetch
  wires --debug request logging and --dns-resolver https DoH resolution
  into the supabase-js client.
- Move the real-backend add→delete round trip to the gated live tier
  (add.live.test.ts) and keep a hermetic e2e for subcommand routing via
  the no-network empty-message path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +21 to +28
const FEEDBACK_STAGING: FeedbackEnvironment = {
url: "https://imrwaufzgcaczqmpnxyr.supabase.co",
key: "sb_publishable_puOyAlqG5J_XfBMTDM2Ckw_L5mieFdb",
};

// No dedicated production feedback project exists yet (CLI-1946): production
// intentionally reuses the staging values until one is provisioned.
const FEEDBACK_PRODUCTION: FeedbackEnvironment = { ...FEEDBACK_STAGING };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: update these values once CLI-1999 and CLI-1998 are complete

@kanadgupta
kanadgupta marked this pull request as ready for review August 19, 2026 00:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8a427eb17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
// The logger's write is synchronous (a gated stderr write); running it
// at this plain-fetch boundary keeps the wrapper a `typeof fetch`.
Effect.runSync(logger.http(method, url));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact deletion tokens from debug URLs

When feedback delete <token> --debug runs, both the preview and DELETE requests encode the capability in the URL as delete_token=eq.<token>, and this call writes that entire URL to stderr. This exposes the token—and therefore authority to read and delete potentially sensitive feedback—in terminal recordings, CI logs, or debug output shared with support; redact this query parameter only for logging while sending the original URL to the transport.

AGENTS.md reference: apps/cli/AGENTS.md:L318-L318

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like an actual risk.

Maybe strip or redact the query string in legacyFeedbackFetch before logger.http(...) the Management API debug logger never has this problem because its credentials ride in headers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the x-feedback-token header is never logged, but the delete_token=eq.<uuid> filter put the same capability in the URL, and legacyFeedbackFetch logged it verbatim. Fixed in 4ab24af: the debug logger now redacts the delete_token query-parameter value before the gated stderr write (the rest of the query string stays, so --debug output remains useful for diagnosing), while the transport receives the original URL. Unit-tested in feedback.layers.unit.test.ts, and the DoH-path integration test asserts the redacted line alongside the un-redacted wire URL. The filter itself stays — it's defense-in-depth under the RLS boundary, and the header remains the actual gate.

Comment on lines +39 to +40
const deleted = await runSupabaseLive(
[

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up live feedback in a finally block

If the add succeeds but JSON parsing, an assertion, or the subsequent delete invocation fails, execution never completes this cleanup and the persistent staging row remains. Fresh evidence in the current revision is that deletion is still only a sequential test step after assertions rather than guaranteed teardown; retain the returned token and perform an exact best-effort delete in finally so failing live runs do not pollute staging.

AGENTS.md reference: AGENTS.md:L170-L170

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid but minor, a leaked row only happens on assertion failure between the add and the delete, staging-only, and the message self-identifies as test data. Fine to wrap the delete in a finally for hygiene, but I wouldn't block on it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 972df73 — the token is captured before the assertions and a best-effort exact-token delete now runs in a finally, so an assertion failure between the add and the delete no longer leaks the staging row. The delete's own assertions stay inside the try so cleanup can't mask a real failure, and the finally delete is inert when the round trip already removed the row.

@kanadgupta
kanadgupta marked this pull request as draft August 19, 2026 00:20
@kanadgupta
kanadgupta marked this pull request as ready for review August 19, 2026 00:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8a427eb17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

*/
export function legacyFeedbackFetch(options: LegacyFeedbackFetchOptions): typeof globalThis.fetch {
const { dnsResolver, logger } = options;
const dohFetch = legacyDohFetch({ dnsResolver, innerFetch: options.innerFetch });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve headers in DoH feedback requests

When --dns-resolver https is used, this wrapper delegates supabase-js requests to legacyDohFetch, but supabase-js supplies init.headers as a Headers instance and legacy-http-dns.ts rebuilds it with { ...init?.headers, Host: ... }; spreading Headers yields no entries. The resulting submit, preview, and delete requests lose apikey, content type, and the x-feedback-* capability headers, so feedback operations fail in the exact environments that need the DoH fallback. Fresh evidence beyond the earlier transport-wiring comment is this incompatibility between the newly wired supabase-js transport and the existing DoH header reconstruction; clone with new Headers(init.headers) and set Host instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ...init?.headers spread in legacyDohFetch preserves apikey/x-feedback-token as-is. Not an issue for this client

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double-checked this one empirically because the spread behaves differently by header representation: supabase-js 2.110.7 passes init.headers as a WHATWG Headers instance, and spreading a Headers instance yields zero entries — Object.keys({ ...headers }) is [] while the real set carries apikey, x-feedback-token, and content-type. (The Management API path was never affected because Effect's Headers type is a plain record — which is also the shape the existing unit test happened to cover.) So Codex is right: with --dns-resolver https, feedback requests lost every header. Fixed in c38af45: legacyDohFetch now rebuilds through the Headers constructor (which accepts records, Headers instances, and entry arrays) and sets Host on the result, with a Headers-instance unit test plus a DoH-path integration test on the real feedback client wiring asserting the capability headers survive the rewrite.

@avallete avallete left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to fix before merge (the open Codex P1 on debug URLs, commented on that thread), one doc fix (PR description still says user_id is never sent), and a couple of non-blocking notes inline.

the PR description says "The access token is never sent; user_id is never sent", but the current code sends the consent-gated gotrue UUID as user_id (add.handler.ts:91), and SIDE_EFFECTS.md documents that correctly. Since the description is what privacy sign-off reads, could you update that bullet to match?

const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
// The logger's write is synchronous (a gated stderr write); running it
// at this plain-fetch boundary keeps the wrapper a `typeof fetch`.
Effect.runSync(logger.http(method, url));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like an actual risk.

Maybe strip or redact the query string in legacyFeedbackFetch before logger.http(...) the Management API debug logger never has this problem because its credentials ride in headers.

*/
export function legacyFeedbackFetch(options: LegacyFeedbackFetchOptions): typeof globalThis.fetch {
const { dnsResolver, logger } = options;
const dohFetch = legacyDohFetch({ dnsResolver, innerFetch: options.innerFetch });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ...init?.headers spread in legacyDohFetch preserves apikey/x-feedback-token as-is. Not an issue for this client

Comment on lines +39 to +40
const deleted = await runSupabaseLive(
[

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid but minor, a leaked row only happens on assertion failure between the add and the delete, staging-only, and the message self-identifies as test data. Fine to wrap the delete in a finally for hygiene, but I wouldn't block on it.

const config = {
message: Argument.string("message").pipe(
Argument.withDescription(
"Freeform feedback. Bare words are joined with spaces. 1000 character limit.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The char limit is documented here but only enforced server-side, so an over-limit message surfaces as a raw PostgREST error classified externalNetwork, so a user mistake gets counted as a backend failure in the actionability KPIs. A client-side length check that fails with an invalidInput classified error before any request would be nice. Non-blocking if you'd rather do it as a follow-up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 04c4e2a rather than deferring — it's a small check and feedback delete already sets the precedent (client-side UUID pre-validation exists for exactly this reason). Over-limit messages now fail before any request with LegacyFeedbackMessageTooLongError classified invalidInput, and the interactive prompt validates the same rule inline. One detail: the length is counted in code points to match Postgres char_length semantics, so the client never rejects a message the server would accept.

kanadgupta and others added 4 commits August 19, 2026 16:36
supabase-js passes init.headers as a Headers instance; spreading one into a
plain object yields zero entries, so the DoH rewrite dropped apikey,
content-type, and x-feedback-token on every feedback request. Rebuild through
the Headers constructor (which accepts records, Headers, and entry arrays)
and cover the Request-embedded case. The Management API path (Effect's
FetchHttpClient) passes a plain record and is byte-identical before/after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The preview/delete URLs carry the row's capability token as a
delete_token=eq.<uuid> PostgREST filter; the --debug logger wrote that URL
verbatim to stderr, leaking read/delete authority into terminal recordings
and shared debug output. Redact the query-param value in the logged line
only — the transport still receives the original URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1000-character limit was documented but only enforced server-side, so an
over-limit message surfaced as a raw PostgREST error classified
externalNetwork — a user mistake counted as a backend failure in the
actionability KPIs. Mirror the check client-side (invalidInput, no request
sent), counted in code points to match Postgres char_length. Same pattern as
feedback delete's client-side UUID pre-validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The staging row was only deleted as a sequential test step, so an assertion
failure between the add and the delete leaked it. Capture the token before
asserting and run a best-effort exact-token delete in finally — inert when
the round trip already removed the row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
let request = client
.from("interfaces_feedback")
.select("feedback")
.eq("delete_token", token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

The delete token is a bearer capability, but it is placed in the PostgREST query string for both preview and deletion. Although the debug wrapper redacts it, HTTPS intermediaries, gateway access logs, tracing, or server request logs can retain delete_token=eq.<token>; anyone obtaining such a log can read and delete the feedback.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: The delete_token is being exposed in URL query parameters (?delete_token=eq.<token>) for both the preview (SELECT) and delete (DELETE) operations, which can be retained in HTTP access logs, proxy logs, CDN/gateway traces, etc.

There are two recommended mitigations:

  1. For the preview (SELECT at line 137): Since RLS already gates row visibility via the x-feedback-token request header (as documented in the file header), the .eq("delete_token", token) URL filter is redundant for authorization. It can be removed from the SELECT query — RLS will correctly limit results to the authorized row via the header alone, keeping the token out of the URL query string.

  2. For the delete (DELETE at line 159): PostgREST's filterless-delete protection prevents removing the .eq() filter. The cleanest solution is to expose a server-side RPC (e.g., delete_interfaces_feedback(token uuid)) analogous to the existing submit_interfaces_feedback RPC already used at line 118. Calling it via client.rpc("delete_interfaces_feedback", { token }) sends the token in the POST request body rather than the URL, eliminating log exposure. If a new RPC is not feasible, consider using a non-sensitive row identifier (e.g., a surrogate primary key returned during submission) as the URL filter for DELETE, keeping the x-feedback-token header as the actual authorization mechanism.

if (Option.isSome(fromEnv)) return fromEnv;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
return yield* legacyReadProjectRefFile(fs, path, workdir).pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

workdir can point to an attacker-controlled checkout, where supabase/.temp/project-ref may be a symlink to a local secret. This read follows the link and sends the entire file as project_ref through feedback add, enabling exfiltration of credentials or other sensitive contents to the remote feedback backend.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Apply the same PROJECT_REF_PATTERN validation boundary that legacyResolveSoftLinkedRef in legacy-linked-state.ts already uses (line 78 of that file). Two changes are needed:

  1. Add the import at the top of the file (line 2 area):

    import { PROJECT_REF_PATTERN } from "../../config/legacy-project-ref.service.ts";
  2. Filter the file ref against the pattern after reading it (lines 19–21). Change:

    return yield* legacyReadProjectRefFile(fs, path, workdir).pipe(
      Effect.orElseSucceed(() => Option.none<string>()),
    );

    to:

    return yield* legacyReadProjectRefFile(fs, path, workdir).pipe(
      Effect.orElseSucceed(() => Option.none<string>()),
      Effect.map(Option.filter((ref) => PROJECT_REF_PATTERN.test(ref))),
    );

This ensures that if supabase/.temp/project-ref is a symlink to a secret file (e.g. ~/.supabase/access-token), its content — which will not match /^[a-z]{20}$/ — is silently discarded instead of being sent as project_ref to the remote feedback backend. This mirrors the explicit comment in legacy-linked-state.ts (lines 50–64) documenting this exact security boundary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 0c11b16 — the resolver now filters both the env/flag value and the file contents through the same PROJECT_REF_PATTERN boundary legacyResolveSoftLinkedRef uses, so a symlinked or corrupted .temp/project-ref degrades to "unlinked" instead of shipping its contents as project_ref (an invalid env override now also falls through to the file, matching legacy-linked-state's precedence). Worth noting the blast radius is narrower than "exfiltration": RLS means an attacker can never read the row back — but secret material leaving the machine at all is reason enough, and this restores consistency with the boundary legacy-linked-state.ts already documents. Integration-tested for both the malformed-file and malformed-env cases.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 129e3d627a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/docs/go-cli-divergences.md Outdated
Comment on lines +18 to +19
| `feedback add` | [`../src/legacy/commands/feedback/add/add.command.ts`](../src/legacy/commands/feedback/add/add.command.ts) | Quick feedback submission, legacy shell only (CLI-1946). Submits through the `submit_interfaces_feedback` RPC via supabase-js with a committed publishable key and prints the server-issued delete token once; profile-driven staging/production environments (production currently reuses staging). `-o` accepts `pretty\|json` only (no Go struct exists for the yaml/toml encoders). |
| `feedback delete` | [`../src/legacy/commands/feedback/delete/delete.command.ts`](../src/legacy/commands/feedback/delete/delete.command.ts) | Deletes previously submitted feedback using the token printed by `feedback add` (CLI-2188). Previews the feedback text, then hard-deletes via the token-gated RLS policy (`x-feedback-token` + optional `x-feedback-project-ref` / `x-feedback-user-id` headers). `-o` accepts `pretty\|json` only (no Go struct exists for the yaml/toml encoders). |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove feedback from the frozen divergence ledger

Remove these two rows: they document brand-new commands in a file reserved as a frozen historical record of the completed TypeScript port. Continuing to add features here undermines that ledger's historical meaning; the new commands are already documented through help text, tests, and their SIDE_EFFECTS.md files, which are the required destinations for new behavior.

AGENTS.md reference: apps/cli/AGENTS.md:L514-L517

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in fead113. The rows predate the ledger freeze (they were added back when AGENTS.md required recording TS-only commands here in the same change; the freeze merged into this branch afterwards), but under the current framing they're new-command docs in a historical record — and everything in them is already covered by the two SIDE_EFFECTS.md files and help text.


## Notes

- TS-only command — no Go CLI counterpart, so no Go-parity constraints apply.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Describe the command without Go-parity framing

Remove or rewrite this newly added note, which is duplicated in the delete command's SIDE_EFFECTS.md: the stable TypeScript shell is now authoritative, so new documentation must describe behavior in its own terms rather than asserting that Go parity or a Go counterpart is irrelevant.

AGENTS.md reference: apps/cli/AGENTS.md:L52-L58

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten in 281e791 — both SIDE_EFFECTS.md notes are gone, and I swept the rest of the feedback family for the same framing per the opportunistic-cleanup rule: feedback-output.ts's "no Go struct" rationale now describes the -o enum restriction in terms of the *.go-payload.ts struct specs it lacks, and the "Go-compat -o json wins" comments in both handlers now cite the legacy shell invariant directly.

agentName,
},
})
.pipe(Effect.tapError(() => sending.fail()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop the feedback spinner on interruption

When Ctrl-C interrupts an in-flight submission, tapError does not run for the interruption cause and the subsequent sending.clear() is skipped. If the delayed Clack spinner has started, it therefore remains active with terminal state such as the hidden cursor unrecovered while telemetry finalizers and shutdown run; if interruption arrives before the delay, the pending timer can start the spinner after cancellation. Manage the task with an interruption-safe finalizer (onExit/ensuring) and apply the same fix to the lookup and deletion tasks.

AGENTS.md reference: AGENTS.md:L64-L66

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mechanism confirmed — tapError skips the interrupt path and the pending 200 ms start timer can fire after cancellation. One severity note: clack's spinner registers its own SIGINT/exit hooks that restore the cursor at process exit, so the residual failure is a cosmetic stray line racing shutdown output rather than a corrupted terminal. Fixed for feedback's three tasks in 6234f41 via an Effect.onExit-based settle helper (feedback-task.ts: cleared on success and interruption, failed on a typed failure), unit-tested including the mid-flight interrupt. Since this is a property of the documented output.task + tapError + clear pattern every legacy command follows, the general fix (an interruption-safe shared combinator, plus making the task handle's settle idempotent) is tracked in CLI-2222 rather than hand-patched per command.


const stdin = yield* Stdin;
if (!stdin.isTTY) {
const piped = yield* stdin.readPipedText;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound piped feedback before collecting it

When stdin contains a very large stream—for example, a user accidentally pipes a multi-gigabyte log—the new command calls readPipedText, whose implementation collects the entire stream into memory, and only afterward applies the 1,000-character limit. This can make the CLI consume unbounded memory or be killed instead of returning the documented length error; consume only enough input to establish that the code-point limit was exceeded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 3c41e19 using the constant-memory pipedBytesStream the Stdin service already exposes: piped input is read up to a 64 KB cap and anything past it fails with the existing LegacyFeedbackMessageTooLongError before further buffering. The cap is 16× the limit's worst-case UTF-8 size (4 bytes/code point), so the established trim-then-count semantics are unchanged for any plausible input; read errors still degrade to "no piped input" exactly like readPipedText did. SIDE_EFFECTS.md documents the cap.

Comment on lines +91 to +94
if (!yes) {
const confirmed = yield* output.promptConfirm("Permanently delete this feedback?", {
defaultValue: false,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-TTY stdin before confirming deletion

When stdin is piped but stdout remains a terminal—for example, printf 'y\n' | supabase feedback delete <token>output.format is still text, so this branch invokes Clack against non-TTY stdin instead of enforcing the documented requirement to pass --yes in non-interactive contexts. Depending on the pipe and Clack behavior, it can consume the piped answer and delete without --yes, cancel, fail, or wait unexpectedly; check stdin TTY state before prompting and return the non-interactive error otherwise.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same class as the add-side prompt gate from the earlier review round — output.interactive is stdout-derived, and clack's confirm answers on a single y/n keypress from any stdin, so printf 'y' | feedback delete <token> with a TTY stdout could confirm a permanent delete without --yes. Fixed in db5e615: the confirm is now gated on stdin.isTTY && output.interactive, failing with NonInteractiveError otherwise — which is what SIDE_EFFECTS.md already documented ("prompt unavailable (non-interactive …) → exit 1"), so the code now matches the contract. Covered with the same pair of asymmetric-TTY integration tests add has, plus a --yes-with-piped-stdin escape-hatch test.

kanadgupta and others added 6 commits August 20, 2026 12:51
The workdir can be an untrusted checkout where supabase/.temp/project-ref is
a symlink to a local secret; the resolver forwarded the raw file contents as
project_ref to the feedback backend. Filter both the env/flag override and
the file contents through PROJECT_REF_PATTERN — the same boundary
legacyResolveSoftLinkedRef applies — degrading anything malformed to
"unlinked".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
output.interactive is stdout-derived and clack's confirm answers on a single
y/n keypress from any stdin, so `printf 'y' | feedback delete <token>` with a
TTY stdout could confirm a permanent delete without --yes. Gate the prompt on
both streams being TTYs and fail with NonInteractiveError otherwise — the
behavior SIDE_EFFECTS.md already documented, and the same gate the add prompt
uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
go-cli-divergences.md is a frozen historical record that no longer
accumulates entries; the feedback commands are new CLI behavior documented
through help text, tests, and their SIDE_EFFECTS.md files. The rows predate
the ledger freeze.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SIDE_EFFECTS notes and -o enum comments framed behavior around Go parity
("no Go counterpart", "Go-compat -o json", "no Go struct"); the TS shell is
the source of truth, so describe the behavior directly. No functional change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tapError skips the interrupt path and the sequential clear() never runs, so
Ctrl-C mid-request left the delayed clack spinner (or its pending start
timer) running while shutdown finalizers executed. Wrap the three request
tasks in an onExit-based settle helper: cleared on success and interruption,
failed on a typed failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
readPipedText collects the entire stream before the 1000-character check ran,
so piping a huge file consumed unbounded memory before failing. Read piped
stdin through pipedBytesStream with a 64 KB cap — 16x the limit's worst-case
UTF-8 size, so trim-then-count semantics are unchanged for plausible input —
and fail as over-limit once the cap is crossed without consuming further.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c41e19d54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
// Both branches (PostgREST rejection, network failure/timeout) are
// failures of the external feedback backend, not user mistakes.
return actionability.externalNetwork;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish backend responses from transport failures

When PostgREST returns a structured HTTP error—for example, an RPC permission rejection or validation failure—run creates this same error class, and this getter records it as externalNetwork just like a timeout or failed fetch. That misclassifies backend/API-status failures as network outages in KPI telemetry; carry a typed failure reason or status from run and select apiStatus for response errors while retaining externalNetwork for transport failures.

AGENTS.md reference: apps/cli/AGENTS.md:L373-L388

Useful? React with 👍 / 👎.

Comment on lines +106 to +108
const confirmed = yield* output.promptConfirm("Permanently delete this feedback?", {
defaultValue: false,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep legacy JSON confirmation output payload-only

When feedback delete <token> -o json runs in a TTY without --yes, the legacy flag leaves output.format === "text", and the quiet-progress wrapper still delegates this Clack prompt, which writes ANSI and prompt text to stdout before the handler emits the raw JSON payload. PTY-based automation therefore cannot parse stdout; the add command has the same problem when -o json reaches its missing-message prompt. Fresh evidence after the earlier payload fix is that both prompts still execute before the goFmt output branch; require noninteractive inputs/--yes for legacy machine mode or render these prompts to stderr.

AGENTS.md reference: apps/cli/AGENTS.md:L442-L450

Useful? React with 👍 / 👎.

return total <= LEGACY_FEEDBACK_PIPE_CAP_BYTES;
}),
),
Effect.catchTag("PlatformError", () => Effect.succeed(undefined)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not submit a partial pipe after a read failure

When piped stdin yields one or more chunks and then fails with PlatformError, this catch converts the stream failure to success but leaves the already buffered parts intact, so the command silently submits the truncated prefix as the user's feedback. Fresh evidence after the bounded-reader fix is that pipedBytesStream intentionally propagates read errors while this consumer resumes from partial state; either discard all buffered input and follow the empty-input path or map the read failure to a typed CLI error instead of sending corrupted feedback.

AGENTS.md reference: AGENTS.md:L64-L68

Useful? React with 👍 / 👎.

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.

2 participants