@sema-agent/core
Version:
Stateless, task-oriented AI agent core
63 lines (62 loc) • 3.82 kB
TypeScript
/**
* design/182 §4.5 (F-011) — adopting a local rule bucket into a cloud principal: a RECOVERABLE state
* machine, not a rename.
*
* "Local first, connect later, keep everything" is the arc this helper carries. It is assembled from
* facts the store already holds — per-add provenance stores NO principal (so a rebind rewrites zero
* rows), the actor is a replica axis orthogonal to identity (so every dot stays valid), and the store
* seam is backend-agnostic (so the same contract covers a PG bucket, whose row-migration half lives on
* the server). What this file adds is ATOMICITY: an exclusive writer lock plus a durable intent marker
* whose `phase` field is a monotonically-advanced position — each mutating stage persists its phase
* BEFORE the next stage runs, so a crash anywhere resumes idempotently and no stage can lose bytes.
*
* ① take the write lock (a concurrent redemption/writer is refused loudly while adoption runs)
* ② land the durable intent marker → phase 2
* ③ rename the bucket file to the principal's → phase 3 (complete ⟺ target present ∧ source absent)
* ④ flip owner resolution — the marker IS the truth `forLocalOwner` reads, so publishing phase 4 is
* itself the switch; host configuration follows the marker, never the reverse → phase 4
* ⑤ first sync round against the (empty) cloud bucket — the full local state rides up → phase 5
* ⑥ rewrite the marker into its PERMANENT terminal record. Never deleted: after phase 4 the marker
* is the owner-resolution truth, and deleting it would resolve local-owner back to a retired empty
* bucket — rules gone from adjudication, new writes forking into a grave.
*
* A pre-existing TARGET bucket refuses the whole adoption: merging two existing buckets is a sync
* join, deliberately not a rename. Rows the far side refuses during ⑤ move to the local quarantine
* area — preserved and disclosed, never deleted (the F-011 "nothing is lost" floor).
*/
import type { RuleOwner } from "../../core/permission-rule-store.js";
import { type PermissionRuleSyncResult, type PermissionRuleSyncTransport } from "../../core/permission-rule-sync.js";
export type AdoptFileRuleStoreResult =
/** The full arc completed (this call, or an earlier one — the terminal marker makes it idempotent). */
{
status: "adopted";
syncResult?: PermissionRuleSyncResult;
}
/** A stage failed. The marker holds the last COMPLETED phase; call again to resume from there.
* No bytes were lost — that is the state machine's whole contract. */
| {
status: "stalled";
phase: 2 | 3 | 4 | 5;
error: string;
};
/**
* Run (or resume) the adoption state machine for the bucket directory `dir`.
*
* Contract notes for the calling deployment:
* - stop (dispose) any live writer over this directory first — the adoption takes the same exclusive
* lock, and a live holder is refused loudly rather than interleaved with;
* - after the arc completes, run tasks under the NEW principal: a deployment still declaring
* local-owner resolves through the terminal marker to the adopted bucket (fail-closed against the
* retired one), but the honest configuration names the principal;
* - pending approval records are NOT migrated: their recorded owner stays historical, so an old
* ticket redeemed after adoption is refused by the existing owner-binding arm — the person simply
* sees the card again. That refusal is the invalidation mechanism, not a defect.
*/
export declare function adoptFilePermissionRuleStore(opts: {
dir: string;
from: RuleOwner;
toPrincipal: string;
transport: PermissionRuleSyncTransport;
now?: () => number;
onError?: (message: string) => void;
}): Promise<AdoptFileRuleStoreResult>;