Skip to content

feat(privacy): add secret-redaction utility, scrub dashboard stopped-output - #191

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
Eyre921:feat/redact-util
Jul 17, 2026
Merged

feat(privacy): add secret-redaction utility, scrub dashboard stopped-output#191
jeff-r2026 merged 1 commit into
Tencent:mainfrom
Eyre921:feat/redact-util

Conversation

@Eyre921

@Eyre921 Eyre921 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a small, dependency-free secret-redaction utility (src/utils/redact.ts) and wires it into the one place TeamAI currently persists raw assistant text — the dashboard's stoppedOutput — so a token pasted into a conversation can no longer land in ~/.teamai/dashboard/events.jsonl (or the rendered dashboard). This is the missing primitive behind the "counts only, no prompt text" posture: with deterministic scrubbing available, later features can safely persist richer session content behind explicit opt-in.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

What it does

redact(text, options) scrubs likely secrets in two layers, applied in order:

  1. Literal masking of known secret values (e.g. credential-shaped env vars via collectEnvSecrets()) — 100% precise for values already in the environment. Longest values are masked first so an overlapping shorter one leaves no fragment.
  2. Regex fallback for common secret shapes — vendor token prefixes (sk-ant-, sk-, ghp_, github_pat_, AWS AKIA…, Slack, Google, Stripe, and a provider catalog), PEM private-key blocks, JWTs, key=value pairs, Authorization: Bearer headers, and connection-string passwords. This catches secrets pasted into a conversation that never touched the environment.

Structural patterns keep surrounding context (password = <REDACTED:kv>, Authorization: Bearer <REDACTED:authz>, postgres://admin:<REDACTED:conn>@host). Redaction runs after any analytics extraction, so metrics are unaffected.

Why vendored rather than a dependency

This runs on the hook hot path (every session event cold-starts the CLI), and the project keeps a small dependency surface and a Node 20 floor. The mature options don't fit that baseline: external scanners (gitleaks, trufflehog) are separate binaries; Node-native linters (secretlint) are dev-time, file-oriented, and require a newer runtime. Those tools use the same regex catalog inlined here, without the framework weight. A heavier external scanner is best layered later as an optional, opt-in deep pass — not the always-on floor.

Test Plan

  • npx tsc --noEmit passes
  • npx vitest run passes — 139 files, 1729 tests
  • Added/updated tests for the change — 21 unit tests in redact.test.ts; a regression test in dashboard-collector.test.ts asserting a ghp_… token in the last assistant message is masked
  • npm run build succeeds
  • Real-CLI E2E: piped a Stop event with a fake ghp_… token through node dist/index.js hook-dispatch stop --tool claude; the resulting events.jsonl stoppedOutput read GH_TOKEN=<REDACTED:gh_tok> and the raw token was absent.

Notes for Reviewers

  • New behavior is limited to readLastAssistantOutput(); everything else is a pure, standalone utility with no callers yet, so blast radius is small.
  • No user-facing CLI/README strings change, so no README.zh-CN.md update is needed.
  • The utility is intentionally best-effort (documented as such) and meant as a reusable building block for privacy-safe session persistence.

…output

Adds a dependency-free `redact()` utility (src/utils/redact.ts) that scrubs
likely secrets from free-form text in two layers:
  1. literal masking of known secret values (e.g. credential-shaped env vars)
  2. regex fallback for common secret shapes — vendor token prefixes, PEM
     private-key blocks, JWTs, key=value pairs, Authorization: Bearer headers,
     and connection-string passwords.

Wires it into readLastAssistantOutput() so the last-assistant snapshot the
dashboard persists to events.jsonl (and renders in the HTML) can no longer
carry a leaked token. Ported from a battle-tested sync hook; runs after any
analytics extraction so metrics are unaffected.

This is the missing primitive behind TeamAI's "counts only, no prompt text"
posture: with deterministic scrubbing in place, later features can safely
persist richer session content behind explicit opt-in.

Test Plan:
- npx tsc --noEmit
- npx vitest run (139 files, 1729 tests pass)
- npm run build
- real-CLI E2E: piped a Stop event with a fake ghp_ token through
  `node dist/index.js hook-dispatch stop`; events.jsonl stoppedOutput came
  out as `GH_TOKEN=<REDACTED:gh_tok>`, raw token absent.

Claude-Session: https://claude.ai/code/session_01GEV81Xj6mhPzSPsyBrDPSd
@jeff-r2026
jeff-r2026 merged commit 0808502 into Tencent:main Jul 17, 2026
7 checks passed
Eyre921 added a commit to Eyre921/teamai-cli that referenced this pull request Jul 17, 2026
…est feed

Implements Features 1 & 5 from docs/designs/team-intelligence-platform.md,
which were accepted ("ENG LOCKED") but never built — digest.ts's
getRecentSessions() has been reading an always-empty sessions/ directory.

`teamai session save` folds the dashboard's existing per-session event stream
(tool sequence, prompt turns, interventions) into a compact markdown summary and
appends it to a local monthly log (~/.teamai/session-logs/<year-month>.md). With
`--push`, a "valuable" session (interventions or substantial tool use, per
Decision 8) is committed to the team repo at sessions/<user>/<month>.md — the
path digest already reads, so "Session Highlights" lights up with no change to
digest.ts.

Reuses existing infrastructure rather than a new collection path:
- dashboard-collector readEvents() + aggregateSessionMetrics() for the data
- utils/git pushRepoDirectly() for the direct team commit (no MR), like contribute
- redact() (from Tencent#191) on the only free text included (the first-prompt line)

Review feedback addressed:
- Placed under a `session` subcommand group (`teamai session save`) instead of a
  top-level verb, matching how `roles` / `source` are organized, leaving room
  for `session list` / `session show` later.
- Team upload defaults to counts + tools only; the redacted first-prompt line is
  opt-in via `--include-prompt`, since redact() is best-effort. Local logs keep it.

Team upload is opt-in; local logs are pruned after 90 days (Feature 5 retention).

Test Plan:
- npx tsc --noEmit
- npx vitest run (142 files, 1759 tests; 14 in session-collector.test.ts)
- npm run build
- real-CLI E2E: seeded ~/.teamai/dashboard/events.jsonl, ran
  `node dist/index.js session save --session-id <sid>`; wrote
  ~/.teamai/session-logs/2026-07.md with folded stats, a `ghp_` token in the
  prompt came out `<REDACTED:gh_tok>`; old `save-session` now errors as unknown;
  `--include-prompt` appears in `session save --help`.

Claude-Session: https://claude.ai/code/session_01GEV81Xj6mhPzSPsyBrDPSd
Eyre921 added a commit to Eyre921/teamai-cli that referenced this pull request Jul 21, 2026
…est feed

Implements Features 1 & 5 from docs/designs/team-intelligence-platform.md,
which were accepted ("ENG LOCKED") but never built — digest.ts's
getRecentSessions() has been reading an always-empty sessions/ directory.

`teamai session save` folds the dashboard's existing per-session event stream
(tool sequence, prompt turns, interventions) into a compact markdown summary and
appends it to a local monthly log (~/.teamai/session-logs/<year-month>.md). With
`--push`, a "valuable" session (interventions or substantial tool use, per
Decision 8) is committed to the team repo at sessions/<user>/<month>.md — the
path digest already reads, so "Session Highlights" lights up with no change to
digest.ts.

Reuses existing infrastructure rather than a new collection path:
- dashboard-collector readEvents() + aggregateSessionMetrics() for the data
- utils/git pushRepoDirectly() for the direct team commit (no MR), like contribute
- redact() (from Tencent#191) on the only free text included (the first-prompt line)

Review feedback addressed (round 1):
- Placed under a `session` subcommand group (`teamai session save`) instead of a
  top-level verb, matching how `roles` / `source` are organized, leaving room
  for `session list` / `session show` later.
- Team upload defaults to counts + tools only; the redacted first-prompt line is
  opt-in via `--include-prompt`, since redact() is best-effort. Local logs keep it.

Review feedback addressed (round 2):
- Fixed user-facing command strings that still said `teamai save-session` (an
  unknown command): the read-only error hint and the retry hint now say
  `teamai session save --push`; aligned docstrings in save-session.ts /
  session-collector.ts / types.ts and the design-doc references.
- `assertNotReadOnly` on the --push path is now wrapped in try/catch: an
  HTTP-mode (read-only) team prints a friendly message and keeps the local log
  instead of throwing an uncaught stack trace.
- Idempotency key is now the full session id in a non-rendering HTML comment
  (`<!-- teamai:session <id> -->`) rather than the 8-char short id, removing the
  ~1-in-4B same-month prefix collision that could silently drop a session.
- Replaced the ad-hoc `Promise.race`/`setTimeout` push guard with the shared
  `withTimeout()` helper (src/utils/async.ts, from Tencent#211), which clears its timer
  in a finally so the process doesn't linger after a fast push.

Team upload is opt-in; local logs are pruned after 90 days (Feature 5 retention).

Test Plan:
- npx tsc --noEmit
- npx vitest run (145 files, 1801 tests; 15 in session-collector.test.ts,
  incl. a new "distinct sessions sharing an 8-char id prefix" case)
- npm run build
- real-CLI E2E: seeded ~/.teamai/dashboard/events.jsonl, ran
  `node dist/index.js session save --session-id <sid>`; wrote
  ~/.teamai/session-logs/2026-07.md with folded stats + full-id marker, a `ghp_`
  token in the prompt came out `<REDACTED:gh_tok>`, a second run was idempotent;
  `--push` against a read-only HTTP team printed the graceful message (exit 0, no
  stack trace); old `save-session` errors as unknown; `--include-prompt` appears
  in `session save --help`.
jeff-r2026 pushed a commit that referenced this pull request Jul 21, 2026
…est feed (#192)

Implements Features 1 & 5 from docs/designs/team-intelligence-platform.md,
which were accepted ("ENG LOCKED") but never built — digest.ts's
getRecentSessions() has been reading an always-empty sessions/ directory.

`teamai session save` folds the dashboard's existing per-session event stream
(tool sequence, prompt turns, interventions) into a compact markdown summary and
appends it to a local monthly log (~/.teamai/session-logs/<year-month>.md). With
`--push`, a "valuable" session (interventions or substantial tool use, per
Decision 8) is committed to the team repo at sessions/<user>/<month>.md — the
path digest already reads, so "Session Highlights" lights up with no change to
digest.ts.

Reuses existing infrastructure rather than a new collection path:
- dashboard-collector readEvents() + aggregateSessionMetrics() for the data
- utils/git pushRepoDirectly() for the direct team commit (no MR), like contribute
- redact() (from #191) on the only free text included (the first-prompt line)

Review feedback addressed (round 1):
- Placed under a `session` subcommand group (`teamai session save`) instead of a
  top-level verb, matching how `roles` / `source` are organized, leaving room
  for `session list` / `session show` later.
- Team upload defaults to counts + tools only; the redacted first-prompt line is
  opt-in via `--include-prompt`, since redact() is best-effort. Local logs keep it.

Review feedback addressed (round 2):
- Fixed user-facing command strings that still said `teamai save-session` (an
  unknown command): the read-only error hint and the retry hint now say
  `teamai session save --push`; aligned docstrings in save-session.ts /
  session-collector.ts / types.ts and the design-doc references.
- `assertNotReadOnly` on the --push path is now wrapped in try/catch: an
  HTTP-mode (read-only) team prints a friendly message and keeps the local log
  instead of throwing an uncaught stack trace.
- Idempotency key is now the full session id in a non-rendering HTML comment
  (`<!-- teamai:session <id> -->`) rather than the 8-char short id, removing the
  ~1-in-4B same-month prefix collision that could silently drop a session.
- Replaced the ad-hoc `Promise.race`/`setTimeout` push guard with the shared
  `withTimeout()` helper (src/utils/async.ts, from #211), which clears its timer
  in a finally so the process doesn't linger after a fast push.

Team upload is opt-in; local logs are pruned after 90 days (Feature 5 retention).

Test Plan:
- npx tsc --noEmit
- npx vitest run (145 files, 1801 tests; 15 in session-collector.test.ts,
  incl. a new "distinct sessions sharing an 8-char id prefix" case)
- npm run build
- real-CLI E2E: seeded ~/.teamai/dashboard/events.jsonl, ran
  `node dist/index.js session save --session-id <sid>`; wrote
  ~/.teamai/session-logs/2026-07.md with folded stats + full-id marker, a `ghp_`
  token in the prompt came out `<REDACTED:gh_tok>`, a second run was idempotent;
  `--push` against a read-only HTTP team printed the graceful message (exit 0, no
  stack trace); old `save-session` errors as unknown; `--include-prompt` appears
  in `session save --help`.

Co-authored-by: Eyre921 <Eyre921@users.noreply.github.com>
@Eyre921
Eyre921 deleted the feat/redact-util branch July 21, 2026 13:22
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