Integrations
Purpose¶
This doc is the contract every connector must satisfy to plug an external system into the Harness and the Org Model: what a connector may read, write, and subscribe to; how data is backfilled and kept fresh; how each external object maps into Org Model nodes and evidence records; who owns each field when two systems disagree; behavior under rate limits; what managers may still do when a connector is down; how connector health is scored; and the checklist a new connector must clear before a tenant can install it. The PRD's stance is "five connectors done completely beat thirty done badly." This doc is what "done completely" means.
Scope / Non-scope¶
In scope
- The connector manifest and runtime contract (capabilities, scopes, auth, sync, health, degradation).
- Sync model: initial backfill, incremental sync, webhooks, polling fallback, reconciliation.
- Canonical mappings for the v1 connector set: Slack, Microsoft Teams, Google Workspace (Calendar, Gmail-as-manager-mailbox, Drive), Linear / Jira, GitHub, Gusto / Rippling / Deel, QuickBooks / Xero / Ramp, Square / Toast, Deputy / 7shifts, Twilio SMS and voice.
- Write-back rules, source-of-record per field, conflict resolution.
- Rate limits, backoff, priority classes, per-tenant API budgets.
- Degradation modes and connector health scoring.
- Edge cases (
EC-INT-*), risk-control loops, and the new-connector checklist.
Out of scope
- The Org Model schema (
02-org-model-spec.md) and its API (17-data-model-and-api-reference.md). - Policy evaluation of the actions writes trigger (
05-policy-and-rule-engine.md); this doc says which writes exist and their default class. - Atoms solver behavior and SMS/voice conversation design (
12-atoms-mode-spec.md,10-communication-engine-rules.md). - Tenant-level security beyond connector scope (
13-security-privacy-compliance.md). - Asana, Notion, BambooHR, Workday, Brex, Stripe, Shopify POS, When I Work, Homebase, IoT feeds: same contract, mappings added per §10.
1. Connector contract¶
A connector is a versioned, tenant-instantiated adapter that runs in its own sandboxed process, holds exactly one tenant's credentials, and speaks to the Harness only through the event bus and the Org Model proposal API. Connectors never talk to a model. Connectors never talk to each other.
1.1 Capabilities¶
| Capability | Meaning | Granted when |
|---|---|---|
read |
Pull entities from the external system into evidence records and Org Model proposals. | Tenant installs the connector. Always required. |
subscribe |
Receive push events (webhooks, event subscriptions, streaming APIs). Treated as a hint to re-read, never as truth. | Installed and the external system supports it. Falls back to polling (§2.4). |
write |
Mutate the external system: post a message, comment on an issue, create a calendar event, submit a PTO request, publish a schedule. Every write is an Action that passed the policy engine. | Only after the owning function has graduated to PROPOSE or above for that tenant (16-rollout-and-operations.md). Write scopes are requested lazily at that moment, not at install. |
1.2 Manifest¶
Every connector ships a manifest. The Harness validates it at build time and re-validates granted scopes against it at install and every 24 hours.
type SystemFamily = "comms" | "work" | "people" | "finance" | "atoms" | "identity";
type Capability = "read" | "write" | "subscribe";
type ScopeTier = "core" | `feature:${string}` | "never";
type ScopeRequest = {
scope: string; // vendor scope string, e.g. "channels:history"
tier: ScopeTier; // core = needed to run; feature = granted when feature enabled; never = documented refusal
capability: Capability; // which capability this scope serves
justification: string; // rendered verbatim on the consent screen and the "what my manager knows" page
dataClassMax: "public" | "internal" | "personal" | "sensitive-personal"; // highest class this scope can surface
};
type EntityMapping = {
external: string; // "slack.message", "linear.issue"
target: "orgmodel" | "evidence";
type: string; // Org Model node type or evidence record type
keyFields: string[]; // fields forming the idempotency key
dropFields: string[]; // fields the connector must strip before emitting (see 13-security-privacy-compliance.md §3)
classifier?: string; // name of a content classifier to run on free-text fields
};
type FieldAuthority = {
field: string; // "Person.employment_status"
sourceOfRecord: "external" | "seasuite";
writeClass?: "AUTONOMOUS" | "PROPOSE_CONFIRM" | "HUMAN_ONLY" | "NEVER";
};
type ConnectorManifest = {
id: string; // "slack" | "linear" | "gusto" ...
version: string; // semver
system: SystemFamily;
capabilities: Capability[];
auth: "oauth2" | "app_install" | "bot_token" | "api_key";
scopes: ScopeRequest[];
entities: EntityMapping[];
authority: FieldAuthority[];
sync: {
backfill: { order: string[]; windows: Record<string, string>; chunk: number };
incremental: { strategy: "cursor" | "updated_since" | "changelog"; interval: string };
subscribe?: { transport: "webhook" | "events_api" | "pubsub"; expectedEventGap: string };
pollFallback: { interval: string; reconcileDaily: true };
};
rateLimit: { defaultBudgetPerMin: number; headers: string[]; retryAfterHeader?: string };
health: { freshnessSlo: string; errorRateSlo: number };
degradation: DegradationProfile; // §6
};
1.3 Scopes and least privilege¶
Rules, enforced in the Harness:
| Rule | Statement |
|---|---|
R-SCOPE-1 |
A connector may only use scopes in its manifest. Granted scopes are diffed against the manifest at install and daily; any extra scope is scope drift and the connector pauses until an admin re-consents (EC-INT-009). |
R-SCOPE-2 |
core scopes are requested at install; feature:* scopes when the tenant enables the feature, with the feature named on the consent screen. |
R-SCOPE-3 |
never scopes are documented with the reason and rendered on the "what my manager knows" page as "this manager cannot read X" (human-to-human DMs, HRIS compensation endpoints, any camera/keystroke/location scope). |
R-SCOPE-4 |
Write scopes are never requested at install (§1.1). |
R-SCOPE-5 |
A scope whose dataClassMax is sensitive-personal is usable only for HUMAN_ONLY packet preparation, and only for explicitly allowed fields. |
R-SCOPE-6 |
Tokens live in the tenant vault (envelope-encrypted with the tenant KEK) and are injected at process start. They never appear in the event bus, evidence, receipts, logs, or model context. |
Worked example, Slack:
| Tier | Scopes | Justification shown to tenant |
|---|---|---|
| core | users:read, users:read.email, channels:read, team:read, im:write, im:history (bot's own DMs only), chat:write, app_mentions:read |
Find the people on the team, open a manager thread with each person, read replies in that thread. |
| feature:channel-reading | channels:history (only channels the bot is a member of), groups:history (only private channels the bot is explicitly invited to), reactions:read |
Read work conversation in channels where the manager has been added, so it can notice blockers without asking. |
| feature:calendar-bridge | users:read (tz field) |
Deliver briefs in your local morning. |
| never | mpim:history and im:history for human-to-human DMs, files:read on private files, admin.*, dnd:read presence tracking |
The manager does not read private messages between people, does not track presence, and has no admin power over the workspace. |
1.4 Runtime isolation¶
One process per (tenant, connector), with CPU/memory caps and an egress allowlist limited to the vendor's API hosts. Outbound interfaces to Sea Suite: emitEvidence, proposeOrgChange, emitEvent, reportHealth. Inbound: executeWrite with a signed Action envelope carrying the policy decision id. Connectors are stateless beyond a checkpoint store keyed (tenant, connector, entity).
2. Sync model¶
2.1 Connector lifecycle¶
stateDiagram-v2
[*] --> UNINSTALLED
UNINSTALLED --> AUTHORIZING: tenant starts install
AUTHORIZING --> BACKFILLING: scopes validated
AUTHORIZING --> UNINSTALLED: consent denied / scope mismatch
BACKFILLING --> LIVE: backfill complete, freshness within SLO
LIVE --> DEGRADED: health below 70 or freshness breach
DEGRADED --> LIVE: health 90 or above for 15 min
DEGRADED --> PAUSED: health below 40 or auth error x3
LIVE --> PAUSED: admin pause / scope drift / kill switch
PAUSED --> AUTHORIZING: re-consent required
PAUSED --> LIVE: admin resume (no re-consent needed)
PAUSED --> REVOKED: token revoked upstream / tenant uninstall
LIVE --> REVOKED: token revoked upstream
REVOKED --> [*]
Transitions emit connector.state_changed events with reason and actor; they are visible on the tenant health dashboard (15-observability-and-runbooks.md).
2.2 Initial backfill¶
Backfill runs during onboarding and the listening period (16-rollout-and-operations.md). It is chunked, resumable, and rate-budgeted so it cannot starve interactive traffic.
Order is fixed: identity → structure → activity. People before channels before messages; users before projects before issues. This guarantees every activity record can be linked to a Person on emission rather than parked as an orphan.
Default windows (tenant-overridable, never wider than the connector's dataClassMax allows):
| Entity family | Window | Rationale |
|---|---|---|
| People, roles, orgs | All current + terminated in last 90 days | Attribution of recent work; retention applies after backfill. |
| Channels / teams / projects | All | Structure is small. |
| Work items (issues, PRs) | All open + closed in last 180 days | Velocity baselines need two quarters. |
| Messages in permitted channels | 90 days | Enough for cadence discovery. |
| Calendar | 60 days back, 90 forward | Cadence discovery, meeting baselines. |
| Finance transactions | Fiscal year to date + prior 12 months | Budget-vs-plan needs a trailing year. |
| POS / shifts / time entries | 26 weeks | Demand forecasting needs seasonality. |
Checkpoint schema:
type SyncCheckpoint = {
tenantId: string;
connectorId: string;
entity: string; // "linear.issue"
phase: "backfill" | "incremental" | "reconcile";
cursor: string | null; // vendor cursor or ISO timestamp watermark
chunkIndex: number;
recordsSeen: number;
lastSuccessAt: string; // ISO
lastAttemptAt: string;
consecutiveErrors: number;
etagCache?: Record<string, string>; // externalId -> version/etag for write CAS
};
2.3 Incremental sync and webhooks¶
sequenceDiagram
participant V as Vendor API
participant W as Webhook Receiver
participant C as Connector (tenant sandbox)
participant B as Event Bus
participant OM as Org Model / Evidence Store
participant H as Harness
V->>W: POST webhook (signed)
W->>W: verify signature, dedupe on event id (24h window)
W-->>V: 200 within 1s
W->>C: enqueue hint with entity, externalId, eventType
C->>V: GET entity by id (truth is the API, not the payload)
V-->>C: entity + etag
C->>C: strip dropFields, run classifier, map to canonical
C->>OM: upsert evidence / propose org change (idempotent on key)
C->>B: emit connector.entity_changed
B->>H: trigger subscribed manager runs (debounced 30s per entity)
Rules:
| Rule | Statement |
|---|---|
R-SYNC-1 |
Webhook as notification, API as truth. The connector never stores payload contents; it re-reads the entity. This kills spoofed-content and stale-payload bugs in one move. |
R-SYNC-2 |
Every upsert is idempotent on (tenantId, connectorId, entity, externalId). Re-delivery is harmless. |
R-SYNC-3 |
The webhook receiver acks in under 1s and does no work; verification failures are counted (RB-13) and never processed. |
R-SYNC-4 |
Incremental sync runs on its interval regardless of webhooks, with a 5-minute updated_since overlap. |
R-SYNC-5 |
Connector-triggered manager runs are debounced per entity (30s) and per person (5 min): a burst of edits produces one run. |
R-SYNC-6 |
Deletes are found by the daily reconcile sweep, because most vendors do not reliably webhook deletes. A missing entity is tombstoned, never removed; references from Goals/Projects are flagged (EC-INT-005). |
2.4 Polling fallback¶
The connector switches from subscribe to poll when any of the following hold, and switches back when the condition clears for 30 minutes:
- Webhook subscription creation fails or the vendor reports it inactive.
- No webhook received for
3 × expectedEventGapwhile the incremental sync shows changes (i.e., the system is active but push is silent). - Signature verification failure rate over 5% in a 10-minute window (possible key rotation).
Polling interval defaults: comms 2 min, work 5 min, people 30 min, finance 30 min, atoms POS 5 min, scheduling 5 min. Polling consumes the same per-tenant rate budget (§5) at backfill priority, so it can never starve an IC reply.
3. Canonical mappings into the Org Model¶
Connectors produce two kinds of output:
- Org Model proposals — candidate
Person,Role,Org,Project,Goal,Cadencenodes and edges. During onboarding these are drafted for the owner to correct (OM-2). After onboarding, structural proposals (a new person, a new project) arePROPOSE_CONFIRMunless the tenant has enabled auto-accept for that connector'sidentityfamily (people from the HRIS are auto-accepted; people from Slack are not). - Evidence records — typed, immutable facts linked to Org Model nodes:
WorkItem,PullRequest,Message,Thread,CalendarEvent,Document,TimeEntry,Shift,PtoRequest,Transaction,Expense,Invoice,Sale,LaborSummary,Call,Sms. Receipts cite evidence records by id. Evidence never carries model rationale; it is what the rationale points at.
Every record carries {tenantId, connectorId, externalId, externalUrl, observedAt, sourceVersion, dataClass}.
3.1 Comms: Slack and Microsoft Teams¶
| External object | Target | Key fields | Notes |
|---|---|---|---|
| user / member | Person proposal |
email, display name, tz, title, is_bot, deleted | Identity resolution §3.7. Bots become Person{kind:"agent"} only if the tenant registers them as managed agents (AG-1); otherwise dropped. |
| channel / team channel | Org hint (not a node) + Thread container |
name, members, purpose | Channel membership is a signal for org membership, never authoritative. Channel purpose text feeds cadence discovery. |
| message in a permitted channel | Message evidence |
author, ts, text, thread_ts, mentions, links, reactions | Text is classified before store; sensitive-personal hits are redacted at ingestion with a redacted:true marker. Edits produce a new version; citations pin the version the manager read. |
| message in the manager's own DM thread | Message evidence, Thread{kind:"manager"} |
as above | The only DMs the connector can read. |
| scheduled message / reminder | — | — | Not ingested. |
| user presence, DND, typing | — | — | never scope. Not requested. |
| Teams meeting transcript | — | — | Not ingested in v1; requires explicit per-meeting opt-in design (open question). |
Cadence discovery: recurring posts (standup, weekly) with a stable weekday/hour in a channel over 4+ weeks produce a Cadence proposal with discovered_from: [Message ids].
3.2 Google Workspace (Calendar, Gmail, Drive)¶
| External object | Target | Key fields | Notes |
|---|---|---|---|
| Calendar event on a person's primary calendar | CalendarEvent evidence |
start/end, attendees (emails), title, recurrence, organizer, visibility | Private events: free/busy only (title and attendees dropped). Recurring events with >3 internal attendees become Cadence proposals. |
| Free/busy | CalendarEvent{kind:"busy"} |
start/end | Used by meeting-by-exception and quiet-hour inference. |
| Gmail (manager's own mailbox) | Message evidence |
from, to, subject, body, thread | Only the manager's mailbox (manager@tenant.seasuite.app), used for vendor chasing and inbound requests. Employee inboxes are never read. |
| Drive document shared with the manager | Document evidence |
id, title, owner, modified, mime | Metadata + text only when the manager account has explicit access. Used for plan/goal docs. |
| Directory user (Workspace Admin SDK) | Person proposal |
email, name, org unit, manager | read only; org unit is an Org hint. |
3.3 Work: Linear / Jira and GitHub¶
| External object | Target | Key fields | Notes |
|---|---|---|---|
| Linear team / Jira project | Org hint + Project proposal |
key, name, lead, members | A Linear team maps to an engineering Org if it matches HRIS structure; otherwise to a Project. Owner corrects during onboarding. |
| Linear project / Jira epic | Project proposal |
name, lead, target date, status, progress | Goals are never auto-created from work tools; a project may link to a Goal. |
| Issue | WorkItem evidence |
id, title, state, assignee, priority, estimate, cycle/sprint, created/updated, blocked_by, labels | Titles and descriptions are classified and injection-scanned (14-evaluation-and-testing.md §5). Cross-team moves keep the same externalId (EC-INT-007). |
| Cycle / sprint | Cadence{kind:"planning"} proposal |
start/end, team | Cycle length feeds capacity model. |
| Comment | Message{channel:"issue"} |
author, body, ts | Used for stall detection ("last human activity"). |
| GitHub repository | Project hint |
name, teams | Only repos the app is installed on. |
| Pull request | PullRequest evidence |
number, author, reviewers, state, opened/merged, review_requested_at, last_activity, draft, size | Bot-authored PRs (dependabot, renovate) are tagged author.kind="bot" and excluded from stall detection; they are aggregated weekly instead. |
| Review / review request | Message{channel:"review"} |
reviewer, state, ts | The 20-hour unreviewed PR rule lives in 05-policy-and-rule-engine.md; this connector just emits the facts. |
| Check run / deployment | WorkItem{kind:"ci"} |
status, conclusion | Failure streaks feed Chief Build Officer's incident cadence. |
| Commit message | — | — | Not ingested as text (injection surface, low signal); commit counts only. |
3.4 People: Gusto / Rippling / Deel¶
| External object | Target | Key fields | Notes |
|---|---|---|---|
| Employee / contractor | Person (auto-accepted, HRIS is identity SoR) |
work email, legal name → Person.legal_name (personal class), preferred name, start date, employment status, employment type, work location, department, manager |
Dropped at ingestion: compensation, bank details, SSN/tax ids, date of birth, home address, demographic/EEO fields, medical/leave reasons, background-check status (EC-INT-010). The connector's drop list is tested in CI against recorded fixtures. |
| Department / team | Org proposal |
name, parent, head | HRIS structure is the default org tree; owner corrects. |
| Job title | Role proposal |
title, level | Level is stored; it is never used as a seniority signal in the Org Model, only as function. |
| PTO policy | Policy{kind:"pto"} |
accrual, caps | Read only. |
| PTO balance | Person.pto_balance (personal) |
hours by type | Visible to the person and the Chief Human Officer for quiet HR. |
| PTO request | PtoRequest evidence |
dates, type, status, approver | Sea Suite may create requests on the IC's explicit ask; approval state is owned by the HRIS. |
| Leave of absence | PtoRequest{kind:"leave"} with reason dropped |
dates only | Reason is sensitive-personal; only "on leave until X" enters the model. |
| Termination / offboarding event | Person.employment_status change |
effective date | Triggers EC-INT-003-style checks and the retention clock. The connector never writes this field (HUMAN_ONLY in the HRIS itself). |
3.5 Finance: QuickBooks / Xero / Ramp¶
| External object | Target | Key fields | Notes |
|---|---|---|---|
| Chart of accounts / classes / departments | Org mapping table |
account, class → Org | Owner maps classes to Orgs during onboarding; unmapped spend lands in an "unallocated" bucket the Chief Fin Officer chases. |
| Transaction / journal line | Transaction evidence |
date, amount, account, class, vendor, memo | Memo text is classified and injection-scanned. Card numbers never ingested. |
| Bill / invoice | Invoice evidence |
vendor, amount, due, status | Feeds cash/runway reporting and vendor chasing. |
| Ramp card transaction | Expense evidence |
cardholder → Person, amount, merchant, category, memo, receipt_present, policy_flags | Receipt images are not ingested; only receipt_present. |
| Ramp reimbursement / approval request | Expense{status:"pending"} |
approver chain | In-policy approvals are the Chief Fin Officer's AUTONOMOUS write (§4.2); over-threshold routes to the approvals inbox. |
| Budget (if the vendor has one) | — | — | The budget plan lives in the Sea Suite budget ledger; vendor budgets are ignored to avoid two sources of record. |
3.6 Atoms: Square / Toast, Deputy / 7shifts, Twilio¶
| External object | Target | Key fields | Notes |
|---|---|---|---|
| Location (POS or scheduling) | Org{kind:"location"} |
name, address, tz, hours | Location tz governs quiet hours and shift math (EC-INT-006). |
| Order / sale (Square, Toast) | Sale evidence, aggregated to 15-min buckets |
ts, gross, net, item count, channel | Customer PII is never ingested; item-level data only as aggregates. Free-text item modifiers are not ingested (injection surface). |
| Labor summary (Toast) / timecard (Square) | LaborSummary, TimeEntry |
person, clock in/out, breaks, role | Break compliance evaluation lives in the scheduling solver; the connector emits raw entries. |
| Employee (POS) | Person proposal |
name, email/phone, role | Lower trust than HRIS; merged by phone/email in §3.7. |
| Shift (Deputy, 7shifts) | Shift evidence |
person, location, start/end, role, status (published/open/swapped), created_by | If the tenant designates the Atoms scheduling solver as SoR, Deputy/7shifts becomes the publishing surface (§4.1). |
| Availability / unavailability | Person.availability (personal) |
windows | |
| Shift swap / drop request | Shift{kind:"swap_request"} |
requester, taker, status | Swaps within labor rules are AUTONOMOUS after graduation (PRD §11). |
| Time-off (scheduling tool) | PtoRequest |
dates | If an HRIS is connected, HRIS wins; the scheduling tool mirrors it. |
| Twilio inbound SMS | Sms evidence + manager thread message |
from (E.164 → Person), body, ts | Unknown numbers are quarantined; no Person, no manager run (EC-INT-012). |
| Twilio outbound SMS status | delivery receipt on the outbound Message |
status, error code | Undelivered briefs count against the brief-delivery SLO. |
| Twilio voice call | Call evidence |
from, to, duration, recording_ref (only if tenant enables and both-party consent handled) | Transcript is personal; stored only with the voice line feature enabled. |
3.7 Identity resolution¶
Persons arrive from up to six systems. Resolution order: (1) the HRIS record is the anchor, matched by case-folded work email; (2) a previously linked external id in Person.identities[]; (3) work email from comms/work tools; (4) E.164 phone (atoms); (5) a name match within the same Org produces a PROPOSE_CONFIRM merge proposal, never an auto-merge (EC-INT-004).
type Identity = { connectorId: string; externalId: string; email?: string; phone?: string; verifiedAt: string };
// Person.identities: Identity[] — each link records how it was verified.
Unmatched users after backfill are listed for the owner as "people the manager can see but doesn't know" and are not managed until resolved (EC-INT-003).
4. Write-back rules and conflict resolution¶
4.1 Source of record per field¶
Sea Suite is the source of record only for management-layer objects. Everything else belongs to the external system; Sea Suite caches, cites, and writes through the connector.
| Field / object | Source of record | Sea Suite may write? | Default write class |
|---|---|---|---|
Person.legal_name, employment_status, employment_type, start_date, work_location |
HRIS | No | NEVER (HRIS fields that are HUMAN_ONLY by nature) |
Person.display_name, pronouns, tz, preferred_channel, brief_window, quiet_hours |
Sea Suite (set by the person) | Yes (person's own thread) | AUTONOMOUS on the person's explicit ask |
Person.manager_of_record, Person.managed_by (Chonk) |
Sea Suite | Yes | PROPOSE_CONFIRM (owner approves) |
Person.pto_balance |
HRIS | No | — |
PtoRequest (create) |
HRIS holds state; Sea Suite creates | Yes | AUTONOMOUS on the IC's explicit ask; approval state is read back |
Org, Role structure |
HRIS (draft) → Sea Suite (approved Org Model) | Yes, in Org Model only | PROPOSE_CONFIRM |
Goal, Plan, Cadence, Policy, Decision, feedback, growth plan |
Sea Suite | Yes | Per 05-policy-and-rule-engine.md |
WorkItem.state, assignee, priority, estimate |
Work tool | Yes | PROPOSE_CONFIRM (assignee/priority), AUTONOMOUS for comments and labels after graduation |
WorkItem comment |
Work tool | Yes | AUTONOMOUS after graduation; subject to outreach cap |
PullRequest review request |
GitHub | Yes | AUTONOMOUS after graduation (chasing a reviewer = requesting review + message) |
CalendarEvent (create) |
Google/Outlook | Yes | AUTONOMOUS with meeting-by-exception rule (IC-5): invite must state the written attempt and reason |
| Slack/Teams message in manager thread | Sea Suite authored, vendor stored | Yes | AUTONOMOUS (subject to outreach cap, quiet hours) |
| Slack/Teams message in a channel | as above | Yes | AUTONOMOUS for ritual write-ups after graduation; PROPOSE_CONFIRM for anything naming a person's performance |
Expense approval |
Ramp | Yes | AUTONOMOUS within policy; PROPOSE_CONFIRM over threshold |
Transaction, Invoice (create/modify) |
Accounting | No | NEVER in v1 (the Chief Fin Officer reports; it does not book) |
| Budget plan / envelope | Sea Suite budget ledger | Yes | PROPOSE_CONFIRM for envelope changes |
Shift publish, swap approval |
Scheduling tool unless tenant sets atoms.solverIsSoR=true |
Yes | PROPOSE_CONFIRM until Atoms scheduling graduates; swap within labor rules AUTONOMOUS after |
TimeEntry edits |
Time clock | No | NEVER (wage/hour is HUMAN_ONLY) |
| Outbound SMS / call | Twilio | Yes | AUTONOMOUS within caps and quiet hours; voice outbound PROPOSE_CONFIRM until the voice line graduates |
4.2 Write execution contract¶
Every write is an Action that has already passed the policy engine. The connector receives a signed envelope and performs a compare-and-set.
type WriteEnvelope = {
actionId: string;
policyDecisionId: string; // proves the policy engine approved this action
autonomyClass: "AUTONOMOUS" | "PROPOSE_CONFIRM"; // HUMAN_ONLY never reaches a connector
confirmedBy?: { personId: string; at: string }; // required when PROPOSE_CONFIRM
target: { entity: string; externalId: string; expectedVersion?: string };
op: "create" | "update" | "comment" | "send";
payload: Record<string, unknown>;
idempotencyKey: string; // hash(actionId) — vendors that support it get it as a header
receiptsRef: string;
deadline: string; // ISO; the write is abandoned (not retried) after this
};
executeWrite(env):
assert verifySignature(env) and env.autonomyClass != HUMAN_ONLY
if env.autonomyClass == PROPOSE_CONFIRM: assert env.confirmedBy
if alreadyExecuted(env.idempotencyKey): return priorResult # exactly-once at our layer
if env.target.expectedVersion:
current = api.get(env.target)
if current.version != env.target.expectedVersion:
return conflict(current) # §4.3
result = api.write(env.op, env.payload, headers={Idempotency-Key: env.idempotencyKey})
recordExecution(env.idempotencyKey, result)
emit audit("connector.write", env, result)
return result
4.3 Conflict resolution¶
Conflicts happen when a human changed the target between the manager's read and its write. Rule: never overwrite a human edit without re-evaluating. Last-writer-wins is forbidden for any field a human can edit.
onConflict(env, current):
diff = fieldsChanged(env.target.snapshot, current)
if disjoint(diff, env.payload.fields):
# human changed other fields; our change is still valid
return retryWithVersion(env, current.version) # max 1 retry
if env.autonomyClass == AUTONOMOUS and isBenignSupersede(diff, env):
# e.g. we wanted to comment "unblocked?" and the human already closed the issue
return abandon(env, reason="superseded_by_human") # logged; no message sent
# the human moved the thing we were about to move
return escalate(env, to="proposal", receipts += [current], reason="conflict_with_human_edit")
isBenignSupersede is a small, tested table: closed/merged/cancelled states supersede chases; a human-assigned reviewer supersedes a review request; a human-published schedule supersedes a pending solver proposal (EC-INT-013). Anything else becomes a PROPOSE_CONFIRM proposal with both versions in the receipts.
5. Rate limits and backoff¶
5.1 Budgets¶
Vendor limits change; the connector treats the manifest's defaultBudgetPerMin as a starting budget and adapts from response headers (X-RateLimit-Remaining, Retry-After, GraphQL complexity cost) on every call. The numbers below are budgeting defaults, to be verified against vendor docs at build time.
| Connector | Default budget | Adaptive signal | Notes |
|---|---|---|---|
| Slack | 50 req/min per method tier; chat.postMessage ~1 msg/s/channel |
Retry-After on 429 |
Sends are the scarce resource; briefs are pre-scheduled to spread. |
| Teams (Graph) | ~4 req/s per app per tenant | Retry-After |
Throttled per resource type. |
| Google Workspace | Per-project daily quota + per-user per-second | 403 rateLimitExceeded, 429 |
Backfill fans out across users. |
| Linear | ~1,500 req/hr per key, complexity-weighted | remaining + complexity headers | One GraphQL query per team per sync. |
| Jira Cloud | ~100 req/min safe | 429 + Retry-After |
JQL updated >= searches. |
| GitHub App | 5,000 req/hr per installation; secondary limits on mutations | X-RateLimit-*, 403 |
Mutations spaced ≥ 1 s. |
| Gusto / Rippling / Deel | Tens/min | 429 | 30-min incremental is enough. |
| QuickBooks / Xero | ~500/min (QBO); 60/min + daily cap (Xero) | 429, Xero problem header | Xero's daily cap forces nightly reconcile. |
| Ramp | ~100 req/min | 429 | |
| Square / Toast / Deputy / 7shifts | Per-endpoint, low | 429 | Reads once per 5 min per location. |
| Twilio | Queue per number; long codes ~1 msg/s; 10DLC by campaign tier | 429, queue length | Registered 10DLC campaign required before US GA. |
Per-tenant budget is min(vendorBudget, fairShare), so one tenant's backfill cannot consume a shared app-level quota (GitHub App, Slack app).
5.2 Priority classes¶
Requests are queued per (tenant, connector) with four priorities. Higher priority preempts lower; lower priorities are the ones that back off first.
| Priority | Examples | Starvation guard |
|---|---|---|
| P0 interactive | Reading a message the IC just sent; writing the reply | Reserved 20% of budget. |
| P1 scheduled outreach | Briefs, cadence write-ups, in-policy approvals | Reserved 30%. |
| P2 detection | Incremental sync, stall detection reads, chases | Remainder. |
| P3 bulk | Backfill, reconcile, reprocessing | Only idle budget; paused entirely when health < 70. |
5.3 Backoff¶
retry(call, priority):
attempt = 0
while attempt < maxAttempts[priority]: # P0: 3, P1: 5, P2: 6, P3: 8
resp = call()
if resp.ok: return resp
if resp.status == 429 or resp.status in (500,502,503,504):
wait = resp.retryAfter or min(cap, base * 2**attempt) * random(0,1) # full jitter
if resp.status == 429: budget.shrink(0.5) # halve budget until a clean 15-min window
sleep(wait); attempt += 1; continue
if resp.status in (401,403): return authFailure(resp) # no retry; health event
return resp
breaker.trip(30s * 2**breaker.trips) # circuit breaker per connector
return exhausted
Base 1s, cap 5 min. A tripped breaker moves the connector to DEGRADED and triggers the degradation profile (§6). Writes with a passed deadline are abandoned, not retried; the Harness decides whether to re-plan.
6. Degradation modes¶
Principle: stale-labeled, never silent. When a connector is degraded, managers keep working from the last good snapshot, but every receipt and every user-facing output built on stale data carries a staleness marker, and actions whose safety depends on freshness are downgraded one autonomy class or suspended.
type DegradationProfile = {
freshnessSlo: string; // "15m" for comms, "1h" for work, "24h" for people/finance
staleActions: Array<{ action: string; when: "stale" | "down"; effect: "downgrade" | "suspend" | "reroute" }>;
fallbackSurface?: string[]; // for comms: ["email", "sms", "app_push"]
};
| Connector down | Managers may still | Managers must not | Fallback / what the manager says |
|---|---|---|---|
| Slack / Teams | Read snapshot; compose briefs and replies; queue outbound with 4h TTL | Send via the dead surface; treat silence as a non-reply (stall clocks pause) | Reroute briefs to the person's fallback surface if configured. Thread shows "Slack is unreachable; I'm on email until it's back." |
| Linear / Jira / GitHub | Plan from a snapshot ≤ 24h old, labeled; answer questions with a staleness note | Chase anyone on stall math (stall detection pauses; false chases are the trust-cliff risk) | Health view shows "work data as of HH:MM". Stall clocks resume on recovery; they never backfill. |
| Google Calendar | Use free/busy snapshot ≤ 24h | Book meetings (suspend) |
Manager proposes times in writing instead. |
| HRIS | Use identity snapshot; answer policy questions from cached Policy |
Create PTO requests, state balances, onboard/offboard (suspend) |
Quiet HR requests are acknowledged and queued with a promise of confirmation. |
| Accounting | Report from a ledger snapshot ≤ 24h, labeled | Present cash/runway as current; runway math suspended past 48h | Chief Fin Officer's update leads with the staleness. |
| Ramp | Approve within policy from a snapshot ≤ 6h | Approve past 6h (downgrade to PROPOSE_CONFIRM) |
Approvals queue with a note to the requester. |
| POS | Use last forecast; run huddles | Re-forecast; propose sales-driven schedule changes (suspend) |
Location health shows "sales data as of…". |
| Scheduling | Serve the cached published schedule by SMS; take swap/call-out requests | Publish or modify shifts; confirm a swap as final (suspend) |
Swaps recorded as pending_replay, replayed with CAS on recovery; the IC is told "tentative until the schedule syncs." |
| Twilio | Deliver via app push / email where present | Assume SMS-only staff received anything | Manager of record notified within 15 min that deskless staff are unreachable. |
| HRIS down > 72h | Continue with cached identities | Auto-accept new people from any source | New people wait as proposals. |
Recovery rule (R-DEG-1): on return to LIVE, the connector runs a reconcile before any manager run consumes the data; queued outbound messages older than their TTL are dropped and logged, not sent (nobody wants Tuesday's brief on Thursday).
7. Connector health scoring¶
Health is a 0–100 score computed per (tenant, connector) every minute from the last 15 minutes of samples.
health = 0.40 * freshness + 0.25 * reliability + 0.15 * auth + 0.10 * push + 0.10 * scope
freshness = clamp(1 - (ageOfNewestSuccessfulSync / freshnessSlo), 0, 1)
reliability = 1 - errorRate # non-2xx excluding 429 that recovered within budget
auth = 1 if last auth check ok else 0
push = 1 if subscribe healthy or not applicable; 0.5 if on poll fallback; 0 if both failing
scope = 1 if grantedScopes ⊆ manifestScopes and coreScopes ⊆ grantedScopes else 0
| Score | State | Effect |
|---|---|---|
| ≥ 90 | LIVE |
Normal. |
| 70–89 | LIVE (warning) |
Dashboard warning; P3 traffic throttled 50%. |
| 40–69 | DEGRADED |
Degradation profile applies; alert SEV3; P3 paused. |
| < 40, or auth = 0 for 3 checks, or scope = 0 | PAUSED |
All writes suspended; alert SEV2; tenant admin notified with the fix. |
Scores are per tenant, so one tenant's revoked token never affects another; fleet medians per connector detect vendor-side outages (RB-01).
8. Edge cases¶
| ID | Scenario | Signal / detection | Rule | Action (autonomy class) | Escalation | Test |
|---|---|---|---|---|---|---|
EC-INT-001 |
Vendor redelivers a webhook 3 times over 2 hours. | Same event id within 24h dedupe window. | R-SYNC-2, R-SYNC-3. |
Drop duplicates; single re-read (AUTONOMOUS). |
None. | Sim org chonk-test-co, FakeLinear.redeliver(eventId, n=3); assert one upsert, zero duplicate manager runs. |
EC-INT-002 |
Slack admin revokes the bot token during a brief send. | 401 on chat.postMessage; auth check fails. |
No retry on 401; connector → PAUSED. |
Remaining briefs reroute to fallback surface if configured, else queued 4h (AUTONOMOUS). |
Tenant admin notified with re-consent link; manager of record notified if > 50% of ICs unreachable. | FakeSlack.revokeToken() mid-brief.run; assert no partial sends repeated after re-auth. |
EC-INT-003 |
A contractor exists in Slack and Linear but not in the HRIS. | Person from comms/work with no HRIS identity after backfill. | Unmatched people are not managed until resolved (§3.7). | Listed to owner as unresolved (PROPOSE_CONFIRM: add as contractor / ignore). |
None. | Persona Alex (contractor); assert no manager thread opened before owner resolves. |
EC-INT-004 |
Two HRIS records share one work email (rehire). | Duplicate key on Person.identities.email. |
Anchor = record with employment_status=active; the other becomes Person.previous_employment. Never auto-merge on name. |
Merge proposal (PROPOSE_CONFIRM). |
Admin. | Fixture with rehire; assert single active Person, history preserved. |
EC-INT-005 |
A Linear project referenced by a Goal is deleted upstream. | Daily reconcile finds the externalId missing. | Tombstone, never delete; Goal edge flagged dangling. |
Chief Porpoise Officer proposes relink or retire Goal (PROPOSE_CONFIRM). |
Goal owner. | FakeLinear.delete(project); assert Goal still resolves with dangling flag. |
EC-INT-006 |
Three restaurant locations in two time zones; POS timestamps arrive in UTC, shifts in local. | Location tz mismatch between POS and scheduling records. | All atoms records normalized to location tz on ingestion; UTC stored alongside. | Reject records with ambiguous tz to a quarantine queue (AUTONOMOUS). |
Ops if quarantine > 1% of records. | chowder-house fixture with Airport location in a different tz; assert huddle times and labor math correct. |
EC-INT-007 |
An issue moves from Team A to Team B in Linear. | Same externalId, changed team. | Identity is the externalId; Org attribution updates; stall clocks reset to the move time. | Update attribution (AUTONOMOUS); no chase for 24h after a move. |
None. | Move issue mid-week; assert no chase to the old assignee. |
EC-INT-009 |
Admin grants the Slack app admin.* scopes by installing a wider manifest. |
Daily scope diff finds scopes not in manifest. | R-SCOPE-1: pause until re-consent with the correct manifest. |
Connector → PAUSED (AUTONOMOUS); explain on the admin page. |
Tenant admin; Sea Suite security if repeated. | FakeSlack.grantExtraScope("admin.users:write"); assert pause and audit event connector.scope_drift. |
EC-INT-010 |
HRIS vendor adds a salary field to the employee payload. |
Unknown field in payload failing the allowlist. | Connectors use field allowlists, not denylists; unknown fields are dropped and counted. | Drop, emit connector.unknown_field metric (AUTONOMOUS). |
Security review if the unknown field name matches the sensitive lexicon. | Fixture adds salary, dob; assert absent from evidence store and model context. |
EC-INT-012 |
An SMS arrives from a number not linked to any Person. | E.164 not in identities. | Quarantine; no manager run; no reply beyond a fixed "this number isn't registered; contact |
Quarantine (AUTONOMOUS). |
Manager of record if the number persists 3 days. | FakeTwilio.inbound(unknown); assert zero model calls. |
EC-INT-013 |
A human publishes shifts in 7shifts while the solver has a pending schedule proposal. | Write conflict: expectedVersion mismatch on publish. |
Human publish supersedes pending solver proposal (isBenignSupersede). |
Withdraw proposal; re-solve against the published schedule; re-propose only deltas (PROPOSE_CONFIRM). |
Location manager of record if the human schedule violates labor rules (flagged, never auto-corrected). | chowder-house W6; assert no double publish and a labor-rule flag if seeded. |
9. Risk controls¶
RC-INT-1 — Over-scoped connector. Owner: Chief Security Officer. Metric: scope-drift events per month (target 0 unresolved > 24h).
- Detect: daily scope diff (R-SCOPE-1); consent-screen scope list compared to manifest at install.
- Contain: connector PAUSED; writes suspended; admin notified.
- Correct: re-consent with correct manifest; if the manifest itself was wrong, hotfix connector version.
- Learn: add the drifted scope to the never list with justification; add fixture to the scope-conformance test.
RC-INT-2 — Stale data drives an action. Owner: Chief Build Officer. Metric: actions executed with any receipt older than the freshness SLO (target 0 for chases; labeled-only for reports).
- Detect: receipts carry maxSourceAge; rule stale_receipt fires when it exceeds the SLO for stale-sensitive actions.
- Contain: downgrade or suspend per degradation profile; stall clocks pause.
- Correct: reconcile on recovery before any run; drop expired queued messages.
- Learn: every stale-driven action that reached a human becomes a scenario keyed to this control.
RC-INT-3 — Write-back clobbers a human edit. Owner: Chief Build Officer. Metric: conflict escalations per 1,000 writes; clobbers (target 0).
- Detect: CAS on every write (expectedVersion); post-write read-back diff.
- Contain: on conflict, abandon or escalate (§4.3); never retry blind more than once.
- Correct: if a clobber is found by read-back, restore the human's version from the etag cache and notify the human with an apology receipt.
- Learn: extend isBenignSupersede table or add a new conflict class; replay the recorded run in CI.
RC-INT-4 — Sensitive field leaks through a payload. Owner: Chief Security Officer. Metric: sensitive-lexicon hits in evidence store (target 0); unknown-field rate.
- Detect: allowlists at ingestion; sensitive-lexicon scan on stored records nightly; connector.unknown_field metric.
- Contain: purge the record from evidence store and any memory derived from it; block the field.
- Correct: connector version bump with the field added to dropFields; retention job re-runs.
- Learn: fixture added; lexicon updated; incident review (15-observability-and-runbooks.md).
10. Checklist for adding a new connector¶
A connector is installable by tenants only when every box is checked and the PR links the evidence.
- [ ] Manifest: every scope tiered (
core/feature:*/never) with a customer-readable justification anddataClassMax; reviewed by the Chief Security Officer domain owner.HUMAN_ONLYfields have no write scope at all. - [ ] Field allowlists (never denylists) per entity;
dropFieldscovers compensation, government ids, DOB, home address, demographics, health/leave reasons, bank details, customer PII, card numbers. Free text is injection-scanned; low-signal free text (commit messages, item modifiers) is not ingested. - [ ] Mapping and authority rows added to §3 and §4.1, with
personalvssensitive-personalmarked and a default write class per writable field. - [ ] Sync: backfill order and windows; incremental with overlap; webhook signature verification and dedupe; polling fallback; daily reconcile; delete → tombstone; idempotent upserts and writes tested with a redelivery fixture.
- [ ] CAS writes with
expectedVersionwhere the vendor supports it, read-back diff where it does not. - [ ] Rate limits, priorities, breaker; load-tested against a fake with the vendor's published limits.
- [ ] Degradation profile and health sampling wired in; verified in the sim org by killing the fake mid-week.
- [ ] Fake implementation with scripted-week hooks (
14-evaluation-and-testing.md§1). - [ ] Identity resolution rules stated; no auto-merge on name.
- [ ] "What my manager knows" strings rendered from the manifest and registered with the page (
INV-READ-1). - [ ] Audit events for install, scope changes, state changes, every write; retention class per entity with deletion tested end to end.
- [ ] Runbook signature (
RB-01variant) and edge cases with scenarios in08-edge-case-catalog.md. - [ ] Vendor review: directory listing, 10DLC / marketplace requirements, terms permitting our use.
- [ ] Shadow run: one internal tenant on
SHADOWfor 2 weeks with zeroSEV2+before general availability.
Open questions¶
- Meeting transcripts and recordings: is there a per-meeting consent design that satisfies "no covert surveillance", or do they stay permanently in
never? - Should the Atoms scheduling solver be SoR by default when Deputy/7shifts is connected? Current default: vendor is SoR, solver publishes through it.
- Accounting write-back is
NEVERin v1. Does the Chief Fin Officer's Full Suite charter need bill-pay scheduling, and at what class? - MCP-first: which v1 vendors have MCP servers mature enough that the connector becomes a thin manifest over MCP, and does that change the sandbox model?
Related docs¶
- System Architecture
- Org Model Spec
- Harness and Orchestration
- Policy and Rule Engine
- Decision Pipeline
- Closed-Loop Risk Mitigation
- Edge Case Catalog
- Communication Engine Rules
- Atoms Mode Spec
- Security, Privacy, Compliance
- Evaluation and Testing
- Observability and Runbooks
- Rollout and Operations
- Data Model and API Reference