Skip to content

feat: Google Chat channel (11th channel) + merge main (1.0.47)#292

Open
anconina wants to merge 203 commits into
mainfrom
feature/v2.33-google-chat-channel
Open

feat: Google Chat channel (11th channel) + merge main (1.0.47)#292
anconina wants to merge 203 commits into
mainfrom
feature/v2.33-google-chat-channel

Conversation

@anconina

@anconina anconina commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Google Chat as the 11th chat channel, end-to-end, and merges the latest main (release 1.0.47 + autonomy/observability work) into the branch.

Google Chat channel

  • Adapter (packages/channels/src/googlechat/): hybrid transport — defaults to a pull model (OAuth-token mint + Pub/Sub pull + Chat REST) with an opt-in signed webhook. Includes the error taxonomy, event→normalized mapper, credential validator, SA-JWT per-scope token provider, bounded/jittered/abortable Pub/Sub pull loop, rich Cards v2 renderer, and send pacer.
  • Config schema (packages/core): channels.googlechat.* incl. mode (pubsub/webhook), audience/audienceType, allowMode/allowFrom.
  • CLI/wizard (packages/cli): interactive prompt + write-path, non-interactive parity (--googlechat-* flags incl. --googlechat-audience-type and SA-key path resolution), and a self-contained googlechat comis doctor check.
  • Validators (packages/daemon): mode-aware credential validation, contained webhook dispatch, groupActivation threading, boot-time audienceType cross-check.
  • Docs: Google Chat runbook + reference, channel-index/media/faq cross-links, channel count 10 → 11 across all channel pages.
  • Self-driving test rig (test/live): Google Chat wire emulator (token + Pub/Sub + Chat REST + JWKS), googlechat-drive.mjs hybrid driver, offline scenario against the real adapter, deterministic ChannelPort double.

Merge of main

Brings in release 1.0.47, MCP-connect diagnosability + credential redaction, orchestrate auto-repair + durable-resume, and autonomy budget default changes. One conflict (test/live/self-driving/scripts/README.md) was resolved to keep both sides — the new googlechat-drive.mjs/emulator notes and main's systemd/phase0-check.sh/GWTOKEN updates.

Test plan

  • pnpm build — clean.
  • Config section-registry-parity + schema-agent-autonomy — 130 passed (snapshot carries both googlechat entries and main's autonomy budget defaults).
  • Daemon autonomy suite — 137 passed.
  • Full pnpm validate run: the only failures were 6 load-induced 5000ms timeouts in packages/channels/src/email/imap-lifecycle*.test.ts — email code is untouched by this branch and those tests pass in isolation (19/19). Flaky under the full parallel coverage run, not a regression. CI will re-run the gates.

🤖 Generated with Claude Code

comis-agent added 30 commits July 5, 2026 14:26
- Per-status classification: undefined->network, 401/403->auth,
  429->platform, >=500->platform, other 4xx->internal
- Secret-safety: a secret-bearing cause never appears in the hint
- RED: errors.ts does not exist yet
- Pure status->errorKind classifier: closed union
  auth|platform|network|precondition|internal + retryable + secret-free hint
- Branch order: undefined->network, 401/403->auth, 429->platform,
  >=500->platform, else->internal
- void cause: the originating error is never rendered into the hint
- No throw for control flow; no @comis/infra import (logger injected elsewhere)
- per-case coverage: both DM encodings (spaceType + legacy type), space/group,
  message.space precedence, senderId fallbacks, argumentText-preferred,
  USER_MENTION -> wasMentioned, thread capture, non-MESSAGE null
- pins the non-empty channelId sentinel (never "") and a parseMessage round-trip
- pure, transport-free Chat interaction event -> NormalizedMessage
- both DM encodings (spaceType DIRECT_MESSAGE and legacy type DM) -> chatType dm
- argumentText preferred over text; senderId from sender.name (users/{id})
- USER_MENTION annotation -> metadata.wasMentioned; thread name captured
- non-empty channelId sentinel keeps a space-name-less event schema-valid
- strict-object defaults (mode pubsub, allowMode allowlist, audienceType
  project-number, missedInboundThresholdMs 6h with 1-min floor)
- serviceAccountKey accepts a plain string OR a SecretRef
- rejects unknown keys incl. an ackReaction key
- ChannelConfigSchema.googlechat self-defaulting wiring
- strict object mirroring MsTeamsChannelEntrySchema: pubsub-default mode,
  serviceAccountKey as SecretRefOrString, allowlist default-deny, 6h
  missed-inbound liveness window floored at 1 min; no ackReaction
- wire googlechat into ChannelConfigSchema (self-defaulting) + export the
  inferred GoogleChatChannelEntry type
- regenerate section-registry parity snapshots (config-metadata surface now
  carries the googlechat entry)
- Assert err names the MISSING field (serviceAccountKey, subscriptionName,
  private_key, client_email), never the secret value
- Assert a missing-field error never echoes the passed key material
- Assert a parse failure carries no raw key text
- Assert email-shaped allowFrom emits an advisory WARN naming users/{id};
  an immutable users/{id} or spaces/{id} entry does not
- Parse-only, secret-safe guard for the SA key JSON + Pub/Sub subscription
- Names the missing field (serviceAccountKey / subscriptionName / private_key
  / client_email), never the secret value; a parse failure carries no raw text
- JSON parse failure handled via try/catch returning a Result (no throw)
- Advisory email-shaped allowFrom lint: WARN with errorKind + hint steering to
  an immutable users/{id}; exempts users/{id} and spaces/{id} entries
…ider

- Assert getToken mints an RS256 SA-JWT assertion (iss=sub=client_email,
  aud=token endpoint, scope, exp-iat<=3600) and POSTs the jwt-bearer grant
- Assert jwtVerify against the SA public key succeeds
- Per-scope cache: reuse within skew, refresh past skew, chat.bot vs pubsub
  cache independently (two scopes => two mints)
- Failure branches (401 status / transport reject) return err with a
  classified errorKind; the SA key, assertion, and minted token never reach
  a log field
- Malformed PKCS#8 and unparseable/incomplete SA key JSON return precondition
  errors that name the requirement, never the key bytes
- createGoogleChatTokenProvider mints an RS256 service-account assertion via
  jose (importPKCS8 + SignJWT): iss=sub=client_email, aud=token endpoint,
  the requested scope, exp-iat capped at 1h, then exchanges it for an access
  token with the jwt-bearer grant at oauth2.googleapis.com/token
- Per-scope expiry+skew cache (bounded 2-entry map): chat.bot and pubsub
  cache independently; reuse within skew, re-mint past it
- Failure branches delegate to classifyGoogleChatError for errorKind+hint;
  the private key, assertion, and minted token never enter a log field
- SA key parsed once; malformed PKCS#8 / unparseable / missing-field keys
  return precondition errors naming the requirement, never the key bytes
- NaN/0 expiry guarded so it cannot poison the cache
- Injected fetch / clock seams; jose is the crypto layer, no hand-rolled JWT;
  reuses the outbound token-provider cache/skew/logging scaffold

Test refined during GREEN: jwtVerify uses currentDate matching the injected
clock (the assertion is signed off the injected now, not wall-clock time).
…ject, dedup on message.name, standard base64

- pollOnce pull-request shape (Bearer, maxMessages, no returnImmediately, abort signal)
- ack fires only after onEvent resolves; a rejecting onEvent skips the ack so Pub/Sub redelivers
- redelivered duplicates dedup on the decoded event.message.name; mark-seen only on the ack path
- standard base64 (not base64url) decode; unparseable payloads acked and skipped; bounded seen-set eviction
- RED: ./pubsub-source.js does not exist yet
- pollOnce POSTs subscription:pull (pubsub Bearer, maxMessages, abort signal), standard base64 decode, JSON.parse
- acks only after onEvent resolves; a rejecting onEvent skips the ack (WARN) so Pub/Sub redelivers
- dedup on the decoded event.message.name; name marked seen only on the ack path; bounded oldest-first eviction
- unparseable payloads acked and skipped; token/transport/status/body failures classified via the shared taxonomy
- start()/stop() are thin lifecycle here; the self-rescheduling backoff loop follows
- bounded jittered exponential backoff (floor..cap) captured via an injected timer seam
- backoff resets to the floor after a good pull
- stop() aborts the in-flight long-poll (AbortController) and cancels a pending backoff timer
- persistent failure logs a loud ERROR (errorKind + hint) and sets lastError; count resets after a good pull
- RED: start() runs no self-rescheduling loop yet
…nt failure

- start() runs a self-rescheduling loop; pull failures back off with jittered exponential delay (floor..cap), reset after a good pull
- abortableSleep shares the AbortController signal with the pull fetch, so stop() aborts an in-flight long-poll and cancels a pending backoff timer
- persistent failure (>= errorLogThreshold consecutive) logs a loud ERROR with errorKind + hint; the count resets after a good pull
- sanctioned/injected systemSetTimeout/systemClearTimeout only — no raw timers or Date.now
…fecycle, status

- default-deny allowlist gate drops non-allowlisted senders before fanout
- open mode and space-id allow admit; non-MESSAGE resolves without a handler
- rejecting/sync-throwing handler rejects (skip-ack) with sibling isolation
- getStatus polling connectionMode + lastInboundAt inbound-only semantics
- start boots the injected source wired to handleChatEvent; stop stops it
- reconcileSend always unresolved; platformAction unsupported; INV omitted methods absent
…, status

- default-deny allowlist gate drops non-allowlisted senders before fanout
- handleChatEvent fans out under a fresh trace context; rethrows on handler
  failure as the pull loop's skip-ack signal, with per-handler isolation
- start validates creds then opens the pull loop wired to handleChatEvent; stop stops it
- getStatus reports polling connectionMode with inbound-only lastInboundAt and source lastError
- reconcileSend always unresolved; platformAction reports unsupported actions
- edit/delete/reaction/attachment methods omitted (capability honesty)
… bearer

- happy path POSTs {text} to the space messages endpoint with a Bearer header
- non-ok status returns err, logs errorKind+hint, and never logs the token
- transport reject classifies network
- an outbound send bumps lastMessageAt but not lastInboundAt
…ot bearer

- mint a chat.bot-scope token, POST {text} to the space messages endpoint
- return the created message resource name on success
- classify a non-ok status / transport reject with a secret-free errorKind+hint; never log the token
- bump lastMessageAt on a successful send (never lastInboundAt)
… CAPABILITIES

- createGoogleChatPlugin identity (channel-googlechat, channelType googlechat) + adapter wrap
- exact text-only feature matrix deep-equal (all false, buttons none)
- maxMessageChars 4000 + replyToMetaKey googlechatMessageName
- capability parity: every false/none flag omits its adapter method
- activate/deactivate delegate to adapter.start/stop; register returns ok
…APABILITIES

- createGoogleChatPlugin(deps) -> ChannelPluginPort (channelType googlechat)
- interim matrix: reactions/editMessages/deleteMessages/fetchHistory/attachments/typing/threads false, buttons none
- limits.maxMessageChars 4000; replyToMetaKey googlechatMessageName
- every false/none flag has its adapter method omitted (capability gate blocks it)
- activate/deactivate delegate to adapter.start/stop; register returns ok(undefined)
… 12)

- add googlechat row (typing:false, threads:false, buttons:none) to EXPECTED
- bump length sanity assert 11 -> 12 and the walk title
- reads googlechat-plugin.ts source; confirms the three flags are own-properties
- adapter (createGoogleChatAdapter + Deps/Handle types) + plugin (createGoogleChatPlugin)
- utilities: mapGoogleChatEventToNormalized/GoogleChatEvent, token provider + CHAT_SCOPE/PUBSUB_SCOPE + auth types
- validateGoogleChatCredentials + classifyGoogleChatError
- surfaces the plugin + validator the daemon wiring consumes
…ecretRef/GOOGLECHAT_SA_KEY resolution

- happy path: config serviceAccountKey + subscriptionName registers the adapter/plugin
- env fallback: GOOGLECHAT_SA_KEY resolves the key when config omits serviceAccountKey
- missing subscription: no registration, secret-free WARN (errorKind + hint, no key)
- bootstrapAdapters registers the googlechat adapter/plugin on enabled+valid config
- SA key resolves as config SecretRef (resolved upstream) or GOOGLECHAT_SA_KEY env fallback
- missing key/subscription: no registration, secret-free WARN (errorKind + hint)
- no gateway ingress (pull loop, not webhook); module doc note 10 -> 11 platforms
- baseline the 12 intra-package-only googlechat exports in public-api-policy (createGoogleChatPlugin + validateGoogleChatCredentials now have the daemon consumer)
…SA_KEY

- config-yaml.mdx: Google Chat (channels.googlechat) key table + shared YAML example entry
- environment-variables.mdx: GOOGLECHAT_SA_KEY as the service-account-key fallback
…ull/non-object decodes

A decoded Pub/Sub payload of the literal JSON null (base64 of "null"
JSON.parses to null, typeof null === "object", null.type throws) reached
the mapper un-guarded and crashed with a TypeError. The pull loop's onEvent
try/catch — meant only for enqueue backpressure — swallowed it as a rejected
enqueue and skip-acked, so Pub/Sub redelivered the poison message forever,
wedging a maxMessages slot every cycle.

Guard the contract boundary: null/non-object events return null (ACK-dropped)
before any deref. Tests pin the map-then-drop contract at all three layers —
mapper, adapter handleChatEvent, and the pull loop's ack-not-skip disposition.
The plugin advertised replyToMetaKey "googlechatMessageName" but the mapper
never wrote it, so inbound-message-id-resolver read undefined and the native id
was never recorded — the bot's replies silently never quoted the triggering
message. Two layers disagreed while message.name was already in hand.

Write metadata.googlechatMessageName = message.name, mirroring how msteams and
discord populate the key they advertise. Tests reconcile all three layers:
mapper output, advertised key, and resolver consumption.
…vent

The rethrow on handler failure is correct behavior — a rejected onEvent promise
is the pull loop's skip-ack (redeliver) signal. But packages/channels/ is not a
raw-throw exempt zone, and this throw only slipped past the guard via its ternary
shape (throw failed.reason ? ...), which the line-anchored regex does not match.
Relying on that shape to evade the guard is convenient-layer evasion.

Add the // @allow-throw: <reason> annotation documenting the contract, so the
sanctioned boundary throw is explicit rather than accidentally-unmatched.
…ot a silent downgrade

mode:"webhook" was accepted and silently ran the Pub/Sub pull loop with only a
vague 'not available yet' WARN (errorKind precondition). The webhook transport
is not wired, so this was both non-functional and misleading.

Emit a config-classed WARN that names the knob (channels.googlechat.mode) and
states plainly that webhook ingress is inactive and the Pub/Sub pull loop is in
use. The schema keeps mode/audienceType/audience — intentionally schema-complete
for the later webhook wiring — but the runtime no longer downgrades silently.
abortableSleep registered an { once: true } abort listener on the shared
controller.signal each cycle; { once: true } only self-removes if abort fires,
so on the normal timer path the listener was never removed. A persistently
failing loop (30s cap) accumulated hundreds of listeners toward Node's
MaxListenersExceededWarning.

Remove the listener when the timer completes normally. A getEventListeners
assertion pins that only the currently-parked sleep's single listener remains
after repeated failure cycles.
comis-agent added 12 commits July 6, 2026 17:25
…text .env round-trips

A Google Chat service-account key is a JSON object whose real-world form is
pretty-printed across many lines. In file storage mode the wizard wrote it to
.env verbatim, but the .env writer is one-line-per-key and the daemon's .env
reader is line-based, so the value was truncated to "{" at boot -- JSON.parse
then threw and the channel never registered, while the wizard's post-write
guard falsely reported success.

Compact the key to single-line JSON in collectManagedSecrets, the single point
both the .env writer and the encrypted store draw from, so it round-trips
losslessly through either. A non-JSON value is left raw so the daemon reports
an honest parse error rather than a silently corrupted key. Strengthen the
mocked write-config test to a multi-line blob asserting a single compacted
.env line.
The --no-auth / --bad-token webhook probes send a forged Bearer, but the only
exit logic was the generic "2xx ack -> 0, else 1", so a correct opaque 401
rejection exited 1 (looked like a failure) and an insecure 2xx accept exited 0
(reported success). The pass/fail was backwards for a security probe: it
flagged the secure outcome as failure and the auth-gate bypass as success, so
it could never catch the regression it exists for. Branch the webhook SEC
probes before the generic check and pass only on status === 401.
…on-interactive

The flag help advertises "or a path to the key file", and the interactive
step resolves a path to its contents. This asserts the non-interactive builder
does the same: given a path, state carries the parseable KEY, not the literal
path string (which would JSON.parse-fail at daemon boot). Adds the inline-JSON
counterpart (a non-path value passes through untouched). Fails today -- the
builder stores the flag verbatim.
…e builder

The flag help advertises "or a path to the key file", and the interactive
step resolves a path to its contents, but the non-interactive builder stored
the flag verbatim -- a path followed the help straight into a boot-time
JSON.parse failure. Extract the interactive path/JSON resolver into a shared
helper and use it in both paths so they cannot drift, giving the flag one
honest contract. write-config's single-line compaction then handles the
multi-line contents a read file carries.
The doc claimed a multiselect of "all 8 supported channels" that "silently
adds IRC". The supported set has grown and IRC is a normal selectable
multiselect option, so state the behavior without the stale count or the
"silently adds IRC" clause.
…ve wizard

In webhook mode the inbound Bearer-JWT verifier selects a different key set
and claim shape per audience type (a Chat-system self-signed token for a
project-number audience, a Google OIDC token bound to the endpoint URL for
app-url). The channel step only prompted for the audience value, so a
wizard-authored webhook config always kept the schema default
audienceType: project-number — and an endpoint-URL audience then silently
rejected every inbound request.

Prompt for the audience type before the audience value, label the audience
prompt per the chosen type, and thread audienceType onto the collected
config (write-config already persists it).
…arity

Mirror the interactive audience-type prompt in non-interactive mode: register
--googlechat-audience-type (project-number|app-url), validate it against the
closed vocabulary, and thread it onto the webhook channel config so a CI-authored
webhook app whose audience is an endpoint URL records audienceType: app-url
instead of keeping the schema default. pubsub omits it (the schema default is
correct there).
The write-config step persists channels.googlechat.audienceType when present,
but no test exercised that branch — the webhook fixture carried no audienceType.
Now that the wizard captures it, drive an app-url webhook config through the
step and assert audienceType reaches the emitted (schema-valid) config block.
…audienceType

The inbound verifier binds to a different key set + claim shape per audienceType
(a Chat-system self-signed token for a numeric project-number audience, a Google
OIDC token for an endpoint-URL app-url audience). A config whose audience shape
contradicts audienceType selects the wrong path and silently rejects every
inbound request, and nothing surfaced it.

Add a content-free advisory lint (webhook mode only, errorKind config) that WARNs
when audienceType is app-url but audience is not URL-shaped, or audienceType is
project-number but audience looks like an endpoint URL. The hint names both
config keys and the fix; validation still passes (advisory only).
…ial validator

The credential validator's audience-shape lint needs audienceType alongside the
audience to fire, but the daemon — its only production caller — passed the audience
without it, so the mismatch stayed silent at boot. Thread
channelConfig.googlechat.audienceType (schema-defaulted, always concrete) into the
validate call so a webhook config whose audience shape contradicts audienceType
surfaces the WARN at startup and via the fleet lens.
…is doctor

The Google Chat doctor check inlines its probes rather than calling the channel
credential validator, so the new audience-shape lint does not reach it for free.
Add a fifth, webhook-only probe that warns when the audience shape contradicts
audienceType (a URL audience declared project-number, or a non-URL audience
declared app-url) — the mismatch that silently rejects every inbound request.
Omitted in pubsub mode; content-free (names the two config keys and the fix).
…chat-channel

# Conflicts:
#	test/live/self-driving/scripts/README.md
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR description incomplete

Please fill in all required sections before this PR can be reviewed.

Required sections: Description, Related Issue, Type of Change, Checklist, RED Test Proof.

For code changes in packages/*/src/**, paste the failing test output (test name + assertion error) from before the production patch in the RED Test Proof section, or write EXEMPT: <reason> for docs/CI/config-only PRs.

See CONTRIBUTING.md for the full contribution bar.

@mintlify

mintlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
comis 🟢 Ready View Preview Jul 7, 2026, 6:53 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

*/
function isEmailShaped(entry: string): boolean {
if (entry.startsWith("users/") || entry.startsWith("spaces/")) return false;
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(entry);
@anconina

anconina commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Code review

Found 1 issue:

  1. The new channels.googlechat.serviceAccountKey field is invisible to the plaintext-secrets audit. It uses the same secret-bearing SecretRefOrStringSchema as botToken/appPassword, but the name does not match SECRET_FIELD_PATTERN in secret-detection.ts (no *key/*credential suffix match), and KNOWN_PROVIDER_PATTERNS in secrets-audit.ts has no GOOGLECHAT_SA_KEY entry (it also misses the generic _API_KEY/_SECRET/_TOKEN/_PASSWORD suffix fallbacks). scanConfigForSecrets decides by field name only, so a raw GCP service-account JSON (with its private_key) pasted into config.yaml produces no PLAINTEXT_SECRET finding from comis doctor — unlike msteams.appPassword or slack.botToken, and unlike the precedent set when MS Teams registered MSTEAMS_APP_PASSWORD in feat(channels): Microsoft Teams channel #277.

New field added by this PR:

export const GoogleChatChannelEntrySchema = z.strictObject({
enabled: z.boolean().default(false),
mode: z.enum(["pubsub", "webhook"]).default("pubsub"),
serviceAccountKey: SecretRefOrStringSchema.optional(),
subscriptionName: z.string().optional(), // pubsub: projects/X/subscriptions/Y

Field-name pattern that misses it:

/**
* Pattern matching field names that contain secrets.
*/
const SECRET_FIELD_PATTERN =
/^(.*token|.*secret|.*password|.*apiKey|.*api_key|.*credential|.*private_key|botToken|appSecret|hmacSecret|webhookSecret)$/i;

Provider patterns missing a GOOGLECHAT_SA_KEY entry:

const KNOWN_PROVIDER_PATTERNS: ReadonlyArray<{ pattern: RegExp; provider: string }> = [
{ pattern: /^ANTHROPIC_API_KEY$/, provider: "anthropic" },
{ pattern: /^OPENAI_API_KEY$/, provider: "openai" },
{ pattern: /^TELEGRAM_BOT_TOKEN$/, provider: "telegram" },
{ pattern: /^DISCORD_BOT_TOKEN$/, provider: "discord" },
{ pattern: /^SLACK_BOT_TOKEN$/, provider: "slack" },
{ pattern: /^SLACK_SIGNING_SECRET$/, provider: "slack" },
{ pattern: /^MSTEAMS_APP_PASSWORD$/, provider: "msteams" },
{ pattern: /^GROQ_API_KEY$/, provider: "groq" },
{ pattern: /^DEEPGRAM_API_KEY$/, provider: "deepgram" },
{ pattern: /^ELEVENLABS_API_KEY$/, provider: "elevenlabs" },
{ pattern: /^BRAVE_API_KEY$/, provider: "brave" },
{ pattern: /^GOOGLE_API_KEY$/, provider: "google" },
{ pattern: /^SECRETS_MASTER_KEY$/, provider: "comis" },
{ pattern: /_API_KEY$/, provider: "unknown" },
{ pattern: /_SECRET$/, provider: "unknown" },
{ pattern: /_TOKEN$/, provider: "unknown" },
{ pattern: /_PASSWORD$/, provider: "unknown" },
];

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

comis-agent added 12 commits July 7, 2026 12:30
…ntKey and GOOGLECHAT_SA_KEY

RED on the pre-patch code: serviceAccountKey matches no generic secret
suffix in SECRET_FIELD_PATTERN, and GOOGLECHAT_SA_KEY ends in _KEY so no
KNOWN_PROVIDER_PATTERNS entry or generic env suffix catches it — a raw
service-account JSON (carrying a private key) in config.yaml or .env
produced zero audit findings while every sibling channel secret flags.
…tion keystone

Add serviceAccountKey to SECRET_FIELD_PATTERN and a GOOGLECHAT_SA_KEY ->
googlechat entry to KNOWN_PROVIDER_PATTERNS.

Threat note: a pasted GCP service-account JSON holds a private key; the
audit's field-name-only walk left it invisible to comis doctor / comis
secrets audit. Fixing the keystone also covers redactForDisplay (config
display), the persist-to-config write firewall, and last-known-good.
Sanctioned refs stay exempt: the wizard writes ${GOOGLECHAT_SA_KEY},
which the full-string env-ref exemption skips.

Verified against ground truth: comis secrets audit on a scratch config
returned [] pre-patch and both findings post-patch.
…tial map

RED on the pre-patch code: buildChannelCredentialMap has an entry for
every other credentialed channel but not GOOGLECHAT_SA_KEY -> googlechat,
so rotating the service-account key never restarts the adapter and it
keeps minting JWTs from the stale key until a manual daemon restart.
The channel-manager's secret:changed subscription looks the rotated
env-var name up in this map to stop/restart the owning adapter; the
googlechat adapter reads the key once at setup, so without the entry a
rotation is silently ignored. Verified against the built dist: the map
now returns googlechat when enabled and stays absent when disabled.
…on advisory for Google Chat

RED on the pre-patch code: the boot-time credential validator WARNs that
groupActivation "always" is inert on Google Chat (the platform only
delivers mentioned/slash-command space messages), but the doctor check
inlines its probes rather than calling the validator, so the advisory
never reached the operator's first troubleshooting command — doctor
reported the channel healthy while the daemon log held the only warning.
…or check

Probe 6 warns when autoReplyEngine.groupActivation is "always", naming
the exact knob and why it cannot broaden delivery on this platform;
omitted for other modes. Corrects the module doc's stale probe count
(said four, enumerated five, now six) and extends the runbook's doctor
bullet, which had also drifted behind the audience-shape probe.

Verified against ground truth: comis doctor on a scratch config with
groupActivation always showed no finding pre-patch and the warn with
suggestion post-patch; mention-gated stays clean.
The closed union gates sendToChannel's channel discriminator; googlechat
was missing, so typed callers could not dead-letter or drain a Google
Chat announcement. Adds the type-level + round-trip test mirroring the
msteams membership test.

RED and GREEN share this commit because the test does not compile
against the pre-patch union: tsc rejects both googlechat sites with
TS2322 before the patch and is clean after.
… policy list

The entry claimed the handle type has no cross-package importer, but the
daemon composition root (setup-channels-adapters.ts) imports and uses it
— the same consumer relationship the sibling MsTeams block records by
omitting MsTeamsAdapterHandle. Listing a consumed export also masks the
policy's job: if the daemon consumer ever disappeared, the stale entry
would keep the orphaned export invisible. public-export-consumers stays
green on the shrink, proving the real consumer is found.
…nels table

The non-interactive init flags for both channels were absent from the
reference table that documents the analogous flags for Telegram,
Discord, Slack, and LINE — a user scripting a headless setup from the
CLI reference could not discover them. Every row is cross-checked
against comis init --help and the schema defaults (authMode secret,
mode pubsub, audienceType project-number).
…chat-channel

# Conflicts:
#	packages/cli/src/commands/init.test.ts
…ressed runs cannot abort mid-flight and leak spy calls into siblings
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