@ref0/cli 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +1744 -814
  2. package/package.json +2 -2
package/dist/main.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
- import { Cause, Clock, Config, ConfigProvider, Console, Context, Cron, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, Match, Option, Path, Predicate, Queue, Random, Redacted, Ref, Result, Runtime, Schedule, Schema, SchemaIssue, SchemaTransformation, Scope, Semaphore, Stdio, Stream, Struct, Terminal, Tuple } from "effect";
4
3
  import { Argument, CliOutput, Command, Flag, Prompt } from "effect/cli";
5
- import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
4
+ import { Cause, Clock, Config, ConfigProvider, Console, Context, Cron, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, Match, Option, Path, Predicate, Queue, Random, Redacted, Ref, Result, Runtime, Schedule, Schema, SchemaIssue, SchemaTransformation, Scope, Semaphore, Stdio, Stream, Struct, Terminal, Tuple } from "effect";
5
+ import { FetchHttpClient, Headers, HttpClient, HttpClientRequest } from "effect/http";
6
6
  import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity } from "effect/http-api";
7
+ import { Base64Url } from "effect/encoding";
7
8
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
8
9
  import { execFile, spawn } from "node:child_process";
9
10
  import { createServer } from "node:http";
@@ -652,6 +653,370 @@ const AgentDetached = eventKind("agent.detached", Schema.Struct({
652
653
  why: Schema.Literals(["revoked", "attach lost"])
653
654
  }));
654
655
  //#endregion
656
+ //#region ../contracts/src/connections.ts
657
+ /**
658
+ * A connection's name in its project, such as `sentry` or `deploys`: lowercase letters, digits
659
+ * and hyphens, from a letter, as a mention names an outside service. `ref0` names ref0's own, so
660
+ * no connection takes it. Two connections of a project are never named alike at once.
661
+ */
662
+ const ConnectionName = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9-]{0,38}$/), Schema.makeFilter((name) => name !== "ref0" || "`ref0` names ref0’s own")).pipe(Schema.brand("ConnectionName"));
663
+ /**
664
+ * What reads a connection's deliveries: `webhook`, whose sender posts ref0's own shape, `{ kind,
665
+ * key, title?, url? }`; or `sentry`, whose sender is Sentry, posting its issue and alert
666
+ * webhooks, which its mapper reads. More kinds come one at a time, each with its mapper.
667
+ */
668
+ const ConnectionKind = Schema.Literals(["webhook", "sentry"]);
669
+ /** The most characters in a signal's link: a longer one is withheld. */
670
+ const signalLinkLength = 2048;
671
+ /**
672
+ * An ID as a signal holds one: letters, digits and `:` `/` `.` `-` `_`, from a letter or a
673
+ * digit, so it can't carry a space, a quote, an instruction or a command's option.
674
+ */
675
+ const signalIdPattern = (length) => new RegExp(`^[A-Za-z0-9][A-Za-z0-9:/._-]{0,${length - 1}}$`);
676
+ /** What happened, in the service's own word: `issue.created`, `deploy.finished`. */
677
+ const SignalKind = Schema.String.check(Schema.isPattern(signalIdPattern(64))).pipe(Schema.brand("SignalKind"));
678
+ /** What it's about, so a repeat can be recognised: an issue's ID, a release. */
679
+ const SignalKey = Schema.String.check(Schema.isPattern(signalIdPattern(128))).pipe(Schema.brand("SignalKey"));
680
+ /**
681
+ * Why a signal's title or link wasn't kept: it looked like a secret; a link that isn't `https`;
682
+ * or a link too long to keep whole.
683
+ */
684
+ const SignalWithheldWhy = Schema.Literals([
685
+ "looks like a secret",
686
+ "not an https link",
687
+ "too long"
688
+ ]);
689
+ /** A signal's title or link the connection gave, withheld, and why. */
690
+ const SignalWithheld = Schema.Struct({
691
+ title: Schema.optionalKey(SignalWithheldWhy),
692
+ url: Schema.optionalKey(SignalWithheldWhy)
693
+ });
694
+ /**
695
+ * A Sentry organization, by its slug as Sentry's URLs name it: lowercase letters, digits,
696
+ * hyphens and underscores, from a letter or a digit.
697
+ */
698
+ const SentryOrg = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9_-]{0,49}$/)).pipe(Schema.brand("SentryOrg"));
699
+ /** Where a Sentry organization's data lives, and so which of Sentry's API hosts answers for it. */
700
+ const SentryRegion = Schema.Literals(["us", "de"]);
701
+ /** A connection added to the project, by the admin whose approval added it. */
702
+ const ConnectionAdded = eventKind("connection.added", Schema.Struct({
703
+ connection: ConnectionId,
704
+ name: ConnectionName,
705
+ kind: ConnectionKind
706
+ }));
707
+ /** A connection's token replaced, by the admin whose approval replaced it: the old one works no more. */
708
+ const ConnectionRotated = eventKind("connection.rotated", Schema.Struct({ connection: ConnectionId }));
709
+ /** A connection removed, by the admin whose approval removed it: its webhook takes nothing more. */
710
+ const ConnectionRemoved = eventKind("connection.removed", Schema.Struct({ connection: ConnectionId }));
711
+ /**
712
+ * A `sentry` connection's credential set, by the admin whose approval set it: the Sentry
713
+ * organization and region it reads, and that ref0 now holds a token for them, which only ref0's
714
+ * server reads, to call Sentry for a workflow. Never the token, nor its hash. Setting it again
715
+ * replaces the one before.
716
+ */
717
+ const ConnectionCredentialSet = eventKind("connection.credential.set", Schema.Struct({
718
+ connection: ConnectionId,
719
+ org: SentryOrg,
720
+ region: SentryRegion
721
+ }));
722
+ /**
723
+ * A signal, received by a connection: delivered to its webhook, by `ref0:hooks`, or sent with
724
+ * `ref0 signal send` by the person or agent whose token sent it. `at` is when ref0 received it.
725
+ * A title or link that failed its check is withheld, and says why in `withheld`; a title too long
726
+ * is cut, and says so in `titleCut`. `dropped` counts the deliveries the connection dropped since
727
+ * its last signal: over a bound, or refused. A delivery's ID, if the service gave one, is in the
728
+ * envelope's `source` only as the SHA-256 of the connection and that ID.
729
+ */
730
+ const SignalReceived = eventKind("signal.received", Schema.Struct({
731
+ connection: ConnectionId,
732
+ kind: SignalKind,
733
+ key: SignalKey,
734
+ title: Schema.optionalKey(TextId),
735
+ url: Schema.optionalKey(TextId),
736
+ withheld: Schema.optionalKey(SignalWithheld),
737
+ titleCut: Schema.optionalKey(Schema.Literal(true)),
738
+ dropped: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
739
+ at: Schema.DateTimeUtcFromMillis
740
+ }));
741
+ /**
742
+ * ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
743
+ * credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
744
+ * such as `https://1.2.3.999`, `https://[1::2::3]` or `https://ref0.test:99999`, so the platform's own URL parser must take
745
+ * it too. It's kept as written, case and any default port too: every path here is appended to it
746
+ * as it is.
747
+ */
748
+ const ApiOrigin = Schema.String.check(Schema.isPattern(/^https?:\/\/(?:\[[0-9A-Fa-f:.]+\]|[^\s/?#\\@:[\]]+)(?::\d+)?$/), Schema.makeFilter((origin) => URL.canParse(origin) || "a URL the platform can parse")).pipe(Schema.brand("ApiOrigin"));
749
+ /** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
750
+ const signInBase = "/auth";
751
+ /**
752
+ * A route under `signInBase`, by its method: its `path` from the origin's root, as a request
753
+ * names it, and its `pathUnderBase`, as Better Auth names its endpoints.
754
+ */
755
+ const signInRoute = (method, pathUnderBase) => ({
756
+ method,
757
+ pathUnderBase,
758
+ path: `${signInBase}${pathUnderBase}`
759
+ });
760
+ /** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
761
+ const signInRoutes = {
762
+ gitHubSignIn: signInRoute("POST", "/sign-in/social"),
763
+ gitHubCallback: signInRoute("GET", "/callback/github"),
764
+ signOut: signInRoute("POST", "/sign-out")
765
+ };
766
+ /**
767
+ * The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
768
+ * token endpoint, registering a public client and revoking a token.
769
+ */
770
+ const oauthRoutes = {
771
+ metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
772
+ authorize: signInRoute("GET", "/oauth2/authorize"),
773
+ token: signInRoute("POST", "/oauth2/token"),
774
+ register: signInRoute("POST", "/oauth2/register"),
775
+ revoke: signInRoute("POST", "/oauth2/revoke")
776
+ };
777
+ signInRoute("POST", "/oauth2/consent");
778
+ [...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
779
+ `${oauthRoutes.metadata.pathUnderBase}`;
780
+ /** Where an agent's session asks for an identity token. */
781
+ const identityTokenPath = "/v1/me/identity-token";
782
+ /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
783
+ const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
784
+ /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
785
+ const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
786
+ /**
787
+ * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
788
+ * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
789
+ * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
790
+ */
791
+ const parsesAsWritten = (audience) => {
792
+ const parts = audiencePattern.exec(audience);
793
+ if (parts === null || !URL.canParse(audience)) return false;
794
+ const url = new URL(audience);
795
+ const port = parts[2] ?? "";
796
+ return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
797
+ };
798
+ /**
799
+ * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
800
+ * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
801
+ * case and any port too, so `https://verifier.example:443` is another audience than
802
+ * `https://verifier.example`. ref0 never fetches or resolves it.
803
+ */
804
+ const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
805
+ /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
806
+ const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
807
+ /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
808
+ const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
809
+ /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
810
+ const IssuedIdentityToken = Schema.Struct({
811
+ token: IdentityToken,
812
+ expiresAt: Schema.DateTimeUtcFromMillis
813
+ });
814
+ /**
815
+ * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
816
+ * token, naming its issuance in ref0's log without naming any row or secret.
817
+ */
818
+ const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
819
+ /**
820
+ * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
821
+ * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
822
+ * audience, different at every other, and carrying no time.
823
+ */
824
+ const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
825
+ /** A session, as one audience knows it, made as a person's is. */
826
+ const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
827
+ /** The accountable person, last in a token's chain: only their pairwise ID. */
828
+ const PersonActor = Schema.Struct({ sub: PairwisePersonId });
829
+ /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
830
+ const sessionActing = (act) => Schema.Struct({
831
+ sub: PairwiseSessionId,
832
+ act
833
+ });
834
+ const oneSession = sessionActing(PersonActor);
835
+ const twoSessions = sessionActing(oneSession);
836
+ const threeSessions = sessionActing(twoSessions);
837
+ /**
838
+ * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
839
+ * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
840
+ * allows the agent in `sub`, never an actor.
841
+ */
842
+ const IdentityActor = Schema.Union([
843
+ oneSession,
844
+ twoSessions,
845
+ threeSessions,
846
+ sessionActing(threeSessions)
847
+ ]);
848
+ /** A time in a token, in whole UTC epoch seconds. */
849
+ const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
850
+ Schema.Struct({
851
+ iss: ApiOrigin,
852
+ sub: AgentId,
853
+ aud: IdentityAudience,
854
+ iat: EpochSeconds,
855
+ exp: EpochSeconds,
856
+ jti: IdentityTokenId,
857
+ act: IdentityActor
858
+ }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
859
+ /**
860
+ * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
861
+ * never names two keys.
862
+ */
863
+ const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
864
+ Schema.Struct({
865
+ alg: Schema.Literal("ES256"),
866
+ kid: SigningKeyId,
867
+ typ: Schema.Literal("JWT")
868
+ });
869
+ /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
870
+ const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
871
+ /** One public key in the JWKS: exactly these fields, and never a private one. */
872
+ const PublicSigningKey = Schema.Struct({
873
+ kty: Schema.Literal("EC"),
874
+ crv: Schema.Literal("P-256"),
875
+ x: Coordinate,
876
+ y: Coordinate,
877
+ use: Schema.Literal("sig"),
878
+ alg: Schema.Literal("ES256"),
879
+ kid: SigningKeyId
880
+ });
881
+ Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
882
+ Schema.Struct({
883
+ issuer: ApiOrigin,
884
+ jwks_uri: Schema.String,
885
+ subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
886
+ id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
887
+ claims_supported: Schema.Tuple([
888
+ Schema.Literal("iss"),
889
+ Schema.Literal("sub"),
890
+ Schema.Literal("aud"),
891
+ Schema.Literal("exp"),
892
+ Schema.Literal("iat"),
893
+ Schema.Literal("jti"),
894
+ Schema.Literal("act")
895
+ ])
896
+ });
897
+ /**
898
+ * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
899
+ * `IdentityAudience` says. The refused text is never echoed.
900
+ */
901
+ var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
902
+ /**
903
+ * Less than a whole second is left before the session, a parent or the credential expires, or
904
+ * the token would expire before it's recorded: the next prompt renews or replaces the session.
905
+ */
906
+ var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
907
+ /**
908
+ * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
909
+ * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
910
+ * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
911
+ * received or used it. A failed append discards the token, so every token has its event.
912
+ */
913
+ const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
914
+ session: SessionId,
915
+ agent: AgentRef,
916
+ /** The person whose attachment authorized the session's chain, last in the token's `act`. */
917
+ accountable: PersonId,
918
+ jti: IdentityTokenId,
919
+ audience: IdentityAudience,
920
+ issuer: ApiOrigin,
921
+ keyId: SigningKeyId,
922
+ issuedAt: Schema.DateTimeUtcFromMillis,
923
+ expiresAt: Schema.DateTimeUtcFromMillis
924
+ }));
925
+ //#endregion
926
+ //#region ../contracts/src/proofs.ts
927
+ /**
928
+ * Proofs of possession: a credential bound to a key, and the proof that comes with each request
929
+ * made with it, RFC 9449's DPoP. A bound credential, a host grant or a session's token, names its
930
+ * key's thumbprint as `cnf`, which only ref0's own state says, never a caller. Each request sends
931
+ * the token as `Authorization: DPoP <token>` and one `DPoP` header: a compact JWS, signed with
932
+ * ES256 by that key, over the request's method and URL, when it was made, and the token's hash.
933
+ * There's no server nonce and no replay store: a captured token and proof can be used again on
934
+ * the same method and path while the proof's time is in the window, and a token alone can't.
935
+ */
936
+ /** A P-256 key's RFC 7638 thumbprint: SHA-256 over its required members, in base64url. */
937
+ const KeyThumbprint = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("KeyThumbprint"));
938
+ /** The key a credential is bound to, by its thumbprint: RFC 7800's `cnf`, as `{ jkt }`. */
939
+ const KeyBinding = Schema.Struct({ jkt: KeyThumbprint });
940
+ /** Hosts ref0's origin may name over plain HTTP, for development on one machine. */
941
+ const loopback = /* @__PURE__ */ new Set([
942
+ "localhost",
943
+ "127.0.0.1",
944
+ "[::1]"
945
+ ]);
946
+ /**
947
+ * ref0's origin as a proof names it: HTTPS, or plain HTTP only on loopback, for development. A
948
+ * client sends a proof, and ref0 takes one, for no other.
949
+ */
950
+ const ProofOrigin = ApiOrigin.check(Schema.makeFilter((origin) => {
951
+ const url = URL.parse(origin);
952
+ return url?.protocol === "https:" || url?.protocol === "http:" && loopback.has(url.hostname) || "HTTPS, or HTTP on loopback";
953
+ })).pipe(Schema.brand("ProofOrigin"));
954
+ /** The most a proof takes, as its compact JWS, before anything else reads it. */
955
+ const dpopProofMaxBytes = 2048;
956
+ const utf8$1 = new TextEncoder();
957
+ const encodeJson$6 = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
958
+ /**
959
+ * An object with exactly `fields`, of at most `maxBytes` as JSON, whatever a decode's options
960
+ * say: an exact `Struct` alone drops a member it doesn't know unless its decode is told to refuse
961
+ * one, so the members are checked on the object as it came, before the struct reads them.
962
+ */
963
+ const exactly = (fields, maxBytes) => {
964
+ const names = new Set(Object.keys(fields));
965
+ return Schema.Record(Schema.String, Schema.Unknown).check(Schema.makeFilter((members) => Object.keys(members).every((name) => names.has(name)) || `only ${[...names].join(", ")}`), Schema.makeFilter((members) => utf8$1.encode(encodeJson$6(members)).byteLength <= maxBytes || `at most ${maxBytes} bytes as JSON`)).pipe(Schema.decodeTo(Schema.Struct(fields), SchemaTransformation.passthroughSupertype()));
966
+ };
967
+ /**
968
+ * A P-256 public key as a JWK: exactly `kty`, `crv`, `x` and `y`, so never a private part, a
969
+ * key reference or another algorithm's key. Whether its point is on the curve is for Web Crypto
970
+ * to say, when it imports it.
971
+ */
972
+ const PublicJwk = exactly({
973
+ kty: Schema.Literal("EC"),
974
+ crv: Schema.Literal("P-256"),
975
+ x: PublicSigningKey.fields.x,
976
+ y: PublicSigningKey.fields.y
977
+ }, 512);
978
+ /** A proof's protected header: its type, ES256, and the public key that signed it. */
979
+ const DpopProofHeader = exactly({
980
+ typ: Schema.Literal("dpop+jwt"),
981
+ alg: Schema.Literal("ES256"),
982
+ jwk: PublicJwk
983
+ }, dpopProofMaxBytes);
984
+ /** A proof's `jti`: a fresh UUIDv4 for each proof. Nothing keeps it to refuse a repeat. */
985
+ const DpopProofId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("DpopProofId"));
986
+ /**
987
+ * A proof's claims: its ID; the request's method, and its URL with no query or fragment, as its
988
+ * client sent it, after the URL was built; when it was made, in whole seconds; and, with a token,
989
+ * `ath`, the SHA-256 of the whole token in base64url.
990
+ */
991
+ const DpopProofClaims = exactly({
992
+ jti: DpopProofId,
993
+ htm: Schema.String.check(Schema.isPattern(/^[A-Z]{1,16}$/)),
994
+ htu: Schema.String.check(Schema.isMaxLength(dpopProofMaxBytes)),
995
+ iat: Schema.Int.check(Schema.isGreaterThan(0)),
996
+ ath: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)))
997
+ }, dpopProofMaxBytes);
998
+ /**
999
+ * A bound credential came with a proof that's missing, malformed, stale, from another key, or
1000
+ * for another request, or came as a bearer token, or a proof came with no `DPoP` credential to
1001
+ * go with it. ref0 answers it before any handler runs, so nothing was done. A machine whose clock
1002
+ * is off signs again once, by the answer's `Date`. Also what a machine answers itself when the key
1003
+ * its credential is bound to is gone, without sending anything.
1004
+ */
1005
+ var InvalidDpopProof = class extends Schema.TaggedError()("InvalidDpopProof", {
1006
+ error: Schema.Literal("invalid_dpop_proof"),
1007
+ nextStep: Schema.String
1008
+ }, { httpApiStatus: 401 }) {};
1009
+ /** The one refusal of a proof, with what to do. */
1010
+ const invalidDpopProof = new InvalidDpopProof({
1011
+ error: "invalid_dpop_proof",
1012
+ nextStep: "If this machine’s key was lost, run ref0 connect in your own terminal, then send the next prompt."
1013
+ });
1014
+ /**
1015
+ * The key store a request's proof is signed in didn't sign it: locked, unavailable or out of
1016
+ * time. Nothing was sent. A client's own, never an answer of ref0's.
1017
+ */
1018
+ var KeyStoreUnavailable = class extends Schema.TaggedError()("KeyStoreUnavailable", { nextStep: Schema.String }) {};
1019
+ //#endregion
655
1020
  //#region ../contracts/src/credential-events.ts
656
1021
  /**
657
1022
  * A person's credentials, in that person's own log. Never the token: only its ID, its kind
@@ -672,7 +1037,9 @@ const TokenDelegated = eventKind("token.delegated", Schema.Struct({
672
1037
  session: Schema.optionalKey(SessionId),
673
1038
  /** The credential this one was made under, when there is one. */
674
1039
  parent: Schema.optionalKey(CredentialRef),
675
- expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis)
1040
+ expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
1041
+ /** The key it's bound to, by its thumbprint, for a host grant or a session's token made with one. */
1042
+ cnf: Schema.optionalKey(KeyBinding)
676
1043
  }));
677
1044
  const TokenRevoked = eventKind("token.revoked", Schema.Struct({
678
1045
  credential: CredentialRef,
@@ -1009,6 +1376,11 @@ const CredentialFacts = Schema.Struct({
1009
1376
  target: Schema.optionalKey(TargetGeneration)
1010
1377
  }),
1011
1378
  expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
1379
+ /**
1380
+ * The key it's bound to, for a host grant or a session's token made with one: every request
1381
+ * with it must carry a proof that key signed. ref0's own state says it, never a caller.
1382
+ */
1383
+ cnf: Schema.optionalKey(KeyBinding),
1012
1384
  /** What it may do, and where. Rows a reader can't decode were left out: they grant nothing. */
1013
1385
  scopes: Schema.Array(Scope$2),
1014
1386
  /**
@@ -2754,92 +3126,6 @@ Schema.Struct({ intent: PublishIntentId });
2754
3126
  Schema.Struct({ intent: PublishIntentId });
2755
3127
  Schema.Union([PublishState, Schema.Struct({ none: Schema.Literal(true) })]);
2756
3128
  //#endregion
2757
- //#region ../contracts/src/connections.ts
2758
- /**
2759
- * A connection's name in its project, such as `sentry` or `deploys`: lowercase letters, digits
2760
- * and hyphens, from a letter, as a mention names an outside service. `ref0` names ref0's own, so
2761
- * no connection takes it. Two connections of a project are never named alike at once.
2762
- */
2763
- const ConnectionName = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9-]{0,38}$/), Schema.makeFilter((name) => name !== "ref0" || "`ref0` names ref0’s own")).pipe(Schema.brand("ConnectionName"));
2764
- /**
2765
- * What reads a connection's deliveries: `webhook`, whose sender posts ref0's own shape, `{ kind,
2766
- * key, title?, url? }`; or `sentry`, whose sender is Sentry, posting its issue and alert
2767
- * webhooks, which its mapper reads. More kinds come one at a time, each with its mapper.
2768
- */
2769
- const ConnectionKind = Schema.Literals(["webhook", "sentry"]);
2770
- /** The most characters in a signal's link: a longer one is withheld. */
2771
- const signalLinkLength = 2048;
2772
- /**
2773
- * An ID as a signal holds one: letters, digits and `:` `/` `.` `-` `_`, from a letter or a
2774
- * digit, so it can't carry a space, a quote, an instruction or a command's option.
2775
- */
2776
- const signalIdPattern = (length) => new RegExp(`^[A-Za-z0-9][A-Za-z0-9:/._-]{0,${length - 1}}$`);
2777
- /** What happened, in the service's own word: `issue.created`, `deploy.finished`. */
2778
- const SignalKind = Schema.String.check(Schema.isPattern(signalIdPattern(64))).pipe(Schema.brand("SignalKind"));
2779
- /** What it's about, so a repeat can be recognised: an issue's ID, a release. */
2780
- const SignalKey = Schema.String.check(Schema.isPattern(signalIdPattern(128))).pipe(Schema.brand("SignalKey"));
2781
- /**
2782
- * Why a signal's title or link wasn't kept: it looked like a secret; a link that isn't `https`;
2783
- * or a link too long to keep whole.
2784
- */
2785
- const SignalWithheldWhy = Schema.Literals([
2786
- "looks like a secret",
2787
- "not an https link",
2788
- "too long"
2789
- ]);
2790
- /** A signal's title or link the connection gave, withheld, and why. */
2791
- const SignalWithheld = Schema.Struct({
2792
- title: Schema.optionalKey(SignalWithheldWhy),
2793
- url: Schema.optionalKey(SignalWithheldWhy)
2794
- });
2795
- /**
2796
- * A Sentry organization, by its slug as Sentry's URLs name it: lowercase letters, digits,
2797
- * hyphens and underscores, from a letter or a digit.
2798
- */
2799
- const SentryOrg = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9_-]{0,49}$/)).pipe(Schema.brand("SentryOrg"));
2800
- /** Where a Sentry organization's data lives, and so which of Sentry's API hosts answers for it. */
2801
- const SentryRegion = Schema.Literals(["us", "de"]);
2802
- /** A connection added to the project, by the admin whose approval added it. */
2803
- const ConnectionAdded = eventKind("connection.added", Schema.Struct({
2804
- connection: ConnectionId,
2805
- name: ConnectionName,
2806
- kind: ConnectionKind
2807
- }));
2808
- /** A connection's token replaced, by the admin whose approval replaced it: the old one works no more. */
2809
- const ConnectionRotated = eventKind("connection.rotated", Schema.Struct({ connection: ConnectionId }));
2810
- /** A connection removed, by the admin whose approval removed it: its webhook takes nothing more. */
2811
- const ConnectionRemoved = eventKind("connection.removed", Schema.Struct({ connection: ConnectionId }));
2812
- /**
2813
- * A `sentry` connection's credential set, by the admin whose approval set it: the Sentry
2814
- * organization and region it reads, and that ref0 now holds a token for them, which only ref0's
2815
- * server reads, to call Sentry for a workflow. Never the token, nor its hash. Setting it again
2816
- * replaces the one before.
2817
- */
2818
- const ConnectionCredentialSet = eventKind("connection.credential.set", Schema.Struct({
2819
- connection: ConnectionId,
2820
- org: SentryOrg,
2821
- region: SentryRegion
2822
- }));
2823
- /**
2824
- * A signal, received by a connection: delivered to its webhook, by `ref0:hooks`, or sent with
2825
- * `ref0 signal send` by the person or agent whose token sent it. `at` is when ref0 received it.
2826
- * A title or link that failed its check is withheld, and says why in `withheld`; a title too long
2827
- * is cut, and says so in `titleCut`. `dropped` counts the deliveries the connection dropped since
2828
- * its last signal: over a bound, or refused. A delivery's ID, if the service gave one, is in the
2829
- * envelope's `source` only as the SHA-256 of the connection and that ID.
2830
- */
2831
- const SignalReceived = eventKind("signal.received", Schema.Struct({
2832
- connection: ConnectionId,
2833
- kind: SignalKind,
2834
- key: SignalKey,
2835
- title: Schema.optionalKey(TextId),
2836
- url: Schema.optionalKey(TextId),
2837
- withheld: Schema.optionalKey(SignalWithheld),
2838
- titleCut: Schema.optionalKey(Schema.Literal(true)),
2839
- dropped: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
2840
- at: Schema.DateTimeUtcFromMillis
2841
- }));
2842
- //#endregion
2843
3129
  //#region ../contracts/src/date-time.ts
2844
3130
  /** A UTC date decoded from a string and encoded in ISO format. */
2845
3131
  const Timestamp = Schema.DateTimeUtcFromString;
@@ -5257,6 +5543,11 @@ Schema.Struct({
5257
5543
  /** For a child session: when its parent's token expires, which the child's never outlives. */
5258
5544
  notAfter: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
5259
5545
  /**
5546
+ * The key the start request's proof was signed by, which the session's token is bound to: as
5547
+ * the Worker checked the proof, never a field of the request. None for a start with no proof.
5548
+ */
5549
+ cnf: Schema.optionalKey(KeyBinding),
5550
+ /**
5260
5551
  * The generations the session token pins, from the host grant that started it, or its parent;
5261
5552
  * and in an organization's project, for a person's own agent, its attachment's authorization's
5262
5553
  * and the agent's target generation there.
@@ -5371,8 +5662,26 @@ Schema.Union([
5371
5662
  NotPermitted,
5372
5663
  DeliveryOutsideWindow
5373
5664
  ]);
5665
+ /**
5666
+ * Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
5667
+ * project's visibility, which a session's routes read who sees the project by.
5668
+ */
5669
+ const SessionPlace = Schema.Struct({
5670
+ owner: OwnerId,
5671
+ project: ProjectId,
5672
+ visibility: Visibility,
5673
+ thread: ThreadId,
5674
+ threadNumber: ThreadNumber,
5675
+ session: SessionId,
5676
+ number: SessionNumber,
5677
+ /** Whether it's still going: not ended, and its lease not run out. */
5678
+ live: Schema.Boolean
5679
+ });
5374
5680
  Schema.Struct({ session: SessionId });
5375
- Schema.Struct({ live: Schema.Boolean });
5681
+ Schema.Union([Schema.Struct({
5682
+ live: Schema.Literal(true),
5683
+ place: SessionPlace
5684
+ }), Schema.Struct({ live: Schema.Literal(false) })]);
5376
5685
  Schema.Struct({
5377
5686
  ...commandFields,
5378
5687
  project: ProjectId,
@@ -5439,6 +5748,11 @@ Schema.Struct({
5439
5748
  session: SessionId,
5440
5749
  pins: SessionPins,
5441
5750
  expiresAt: Schema.DateTimeUtcFromMillis,
5751
+ /**
5752
+ * The key the session's start was signed by, which its token is bound to. A parent bound to a
5753
+ * key is bound to this one, or the token isn't registered.
5754
+ */
5755
+ cnf: Schema.optionalKey(KeyBinding),
5442
5756
  cause: Schema.Struct({
5443
5757
  log: ProjectId,
5444
5758
  event: EventId
@@ -6379,8 +6693,8 @@ var ApprovalStale = class extends Schema.TaggedError()("ApprovalStale", { nextSt
6379
6693
  var PreviewNotReady = class extends Schema.TaggedError()("PreviewNotReady", { nextStep: Schema.String }, { httpApiStatus: 503 }) {};
6380
6694
  //#endregion
6381
6695
  //#region ../contracts/src/passkeys.ts
6382
- const Base64Url = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]*$/));
6383
- Schema.Struct({ challenge: Base64Url.check(Schema.isMinLength(1)) });
6696
+ const Base64Url$1 = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]*$/));
6697
+ Schema.Struct({ challenge: Base64Url$1.check(Schema.isMinLength(1)) });
6384
6698
  /** How a browser reaches a passkey's authenticator, as WebAuthn names it. */
6385
6699
  const PasskeyTransport = Schema.Literals([
6386
6700
  "ble",
@@ -6392,7 +6706,7 @@ const PasskeyTransport = Schema.Literals([
6392
6706
  "usb"
6393
6707
  ]);
6394
6708
  const CredentialDescriptor = Schema.Struct({
6395
- id: Base64Url,
6709
+ id: Base64Url$1,
6396
6710
  type: Schema.Literal("public-key"),
6397
6711
  transports: Schema.optionalKey(Schema.Array(PasskeyTransport))
6398
6712
  });
@@ -6403,11 +6717,11 @@ const PasskeyCreationOptions = Schema.Struct({
6403
6717
  name: Schema.String
6404
6718
  }),
6405
6719
  user: Schema.Struct({
6406
- id: Base64Url,
6720
+ id: Base64Url$1,
6407
6721
  name: Schema.String,
6408
6722
  displayName: Schema.String
6409
6723
  }),
6410
- challenge: Base64Url,
6724
+ challenge: Base64Url$1,
6411
6725
  pubKeyCredParams: Schema.Array(Schema.Struct({
6412
6726
  type: Schema.Literal("public-key"),
6413
6727
  alg: Schema.Int
@@ -6446,23 +6760,23 @@ const PasskeyCreationOptions = Schema.Struct({
6446
6760
  * as its WebAuthn library leaves it.
6447
6761
  */
6448
6762
  const PasskeyCreated = Schema.Struct({
6449
- id: Base64Url,
6450
- rawId: Base64Url,
6763
+ id: Base64Url$1,
6764
+ rawId: Base64Url$1,
6451
6765
  type: Schema.Literal("public-key"),
6452
6766
  response: Schema.Struct({
6453
- clientDataJSON: Base64Url,
6454
- attestationObject: Base64Url,
6455
- authenticatorData: Schema.optional(Base64Url),
6767
+ clientDataJSON: Base64Url$1,
6768
+ attestationObject: Base64Url$1,
6769
+ authenticatorData: Schema.optional(Base64Url$1),
6456
6770
  transports: Schema.optional(Schema.Array(PasskeyTransport)),
6457
6771
  publicKeyAlgorithm: Schema.optional(Schema.Int),
6458
- publicKey: Schema.optional(Base64Url)
6772
+ publicKey: Schema.optional(Base64Url$1)
6459
6773
  }),
6460
6774
  authenticatorAttachment: Schema.optional(Schema.Literals(["platform", "cross-platform"])),
6461
6775
  clientExtensionResults: Schema.Struct({ credProps: Schema.optionalKey(Schema.Struct({ rk: Schema.optionalKey(Schema.Boolean) })) })
6462
6776
  });
6463
6777
  /** What ref0 hands a browser to answer with a passkey: WebAuthn's request options, as JSON. */
6464
6778
  const PasskeyRequestOptions = Schema.Struct({
6465
- challenge: Base64Url,
6779
+ challenge: Base64Url$1,
6466
6780
  timeout: Schema.optionalKey(Schema.Int),
6467
6781
  rpId: Schema.optionalKey(Schema.String),
6468
6782
  allowCredentials: Schema.optionalKey(Schema.Array(CredentialDescriptor)),
@@ -6482,14 +6796,14 @@ const PasskeyRequestOptions = Schema.Struct({
6482
6796
  * as JSON. A field the browser doesn't have may be missing, or there as `undefined`.
6483
6797
  */
6484
6798
  const PasskeyAsserted = Schema.Struct({
6485
- id: Base64Url,
6486
- rawId: Base64Url,
6799
+ id: Base64Url$1,
6800
+ rawId: Base64Url$1,
6487
6801
  type: Schema.Literal("public-key"),
6488
6802
  response: Schema.Struct({
6489
- clientDataJSON: Base64Url,
6490
- authenticatorData: Base64Url,
6491
- signature: Base64Url,
6492
- userHandle: Schema.optional(Base64Url)
6803
+ clientDataJSON: Base64Url$1,
6804
+ authenticatorData: Base64Url$1,
6805
+ signature: Base64Url$1,
6806
+ userHandle: Schema.optional(Base64Url$1)
6493
6807
  }),
6494
6808
  authenticatorAttachment: Schema.optional(Schema.Literals(["platform", "cross-platform"])),
6495
6809
  clientExtensionResults: Schema.Struct({})
@@ -6543,10 +6857,15 @@ const RequestFolder = Schema.Struct({
6543
6857
  secretHash: SecretHash,
6544
6858
  replaces: Schema.optionalKey(TokenId)
6545
6859
  });
6546
- /** One to eight config folders, each with its own grant secret. */
6860
+ /**
6861
+ * One to eight config folders, each with its own grant secret, and the machine key its grants are
6862
+ * bound to: its public half, which a proof that key signed comes with. A request without one makes
6863
+ * grants bound to no key, as an older ref0 asks, until binding is required.
6864
+ */
6547
6865
  const RequestAttachment = Schema.Struct({
6548
6866
  machine: MachineId,
6549
6867
  machineName: MachineName,
6868
+ publicKey: Schema.optionalKey(PublicJwk),
6550
6869
  folders: Schema.Array(RequestFolder).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
6551
6870
  collectHash: SecretHash,
6552
6871
  linkHash: SecretHash,
@@ -6740,6 +7059,7 @@ Schema.Struct({
6740
7059
  request: ApprovalId,
6741
7060
  machine: MachineId,
6742
7061
  machineName: MachineName,
7062
+ cnf: Schema.optionalKey(KeyBinding),
6743
7063
  browserSession: TokenId,
6744
7064
  personGeneration: Generation,
6745
7065
  folders: Schema.Array(ConnectFolderCommand).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
@@ -10345,45 +10665,6 @@ Schema.Union([Schema.Struct({
10345
10665
  found: Schema.Literal(true),
10346
10666
  thread: ThreadId
10347
10667
  }), Schema.Struct({ found: Schema.Literal(false) })]);
10348
- /**
10349
- * ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
10350
- * credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
10351
- * such as `https://1.2.3.999`, `https://[1::2::3]` or `https://ref0.test:99999`, so the platform's own URL parser must take
10352
- * it too. It's kept as written, case and any default port too: every path here is appended to it
10353
- * as it is.
10354
- */
10355
- const ApiOrigin = Schema.String.check(Schema.isPattern(/^https?:\/\/(?:\[[0-9A-Fa-f:.]+\]|[^\s/?#\\@:[\]]+)(?::\d+)?$/), Schema.makeFilter((origin) => URL.canParse(origin) || "a URL the platform can parse")).pipe(Schema.brand("ApiOrigin"));
10356
- /** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
10357
- const signInBase = "/auth";
10358
- /**
10359
- * A route under `signInBase`, by its method: its `path` from the origin's root, as a request
10360
- * names it, and its `pathUnderBase`, as Better Auth names its endpoints.
10361
- */
10362
- const signInRoute = (method, pathUnderBase) => ({
10363
- method,
10364
- pathUnderBase,
10365
- path: `${signInBase}${pathUnderBase}`
10366
- });
10367
- /** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
10368
- const signInRoutes = {
10369
- gitHubSignIn: signInRoute("POST", "/sign-in/social"),
10370
- gitHubCallback: signInRoute("GET", "/callback/github"),
10371
- signOut: signInRoute("POST", "/sign-out")
10372
- };
10373
- /**
10374
- * The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
10375
- * token endpoint, registering a public client and revoking a token.
10376
- */
10377
- const oauthRoutes = {
10378
- metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
10379
- authorize: signInRoute("GET", "/oauth2/authorize"),
10380
- token: signInRoute("POST", "/oauth2/token"),
10381
- register: signInRoute("POST", "/oauth2/register"),
10382
- revoke: signInRoute("POST", "/oauth2/revoke")
10383
- };
10384
- signInRoute("POST", "/oauth2/consent");
10385
- [...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
10386
- `${oauthRoutes.metadata.pathUnderBase}`;
10387
10668
  //#endregion
10388
10669
  //#region ../contracts/src/live-calls.ts
10389
10670
  /**
@@ -11032,22 +11313,75 @@ const TurnTokens = Schema.Union([Schema.Struct({
11032
11313
  known: Schema.Literal("read"),
11033
11314
  models: Schema.Array(ModelTokens).check(Schema.isMaxLength(16), Schema.makeFilter((models) => new Set(models.map((each) => each.model)).size === models.length || "each model once"))
11034
11315
  }), Schema.Struct({ known: Schema.Literal("unknown") })]);
11316
+ const Name = Schema.String.check(Schema.isBetweenLength(1, 128));
11317
+ const Millis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
11318
+ /**
11319
+ * How long a turn's tool calls ran, summed, and its longest call, by tool: Claude Code's own
11320
+ * `duration_ms` for each, and, for Codex, whose hooks say none, from its `PreToolUse` to its
11321
+ * `PostToolUse` by `ref0 mcp`'s clock. Only calls that ended count: one with no end, as one a
11322
+ * `PreToolUse` answer denied or a Codex call cut short, isn't in it. `unknown`, never zero, when a
11323
+ * call that ended can't be timed, as one whose end says no time and whose start wasn't seen.
11324
+ */
11325
+ const ToolTime = Schema.Union([Schema.Struct({
11326
+ known: Schema.Literal("timed"),
11327
+ totalMs: Millis,
11328
+ /** None when the turn called no tool. */
11329
+ longest: Schema.optionalKey(Schema.Struct({
11330
+ tool: Name,
11331
+ ms: Millis
11332
+ }))
11333
+ }), Schema.Struct({ known: Schema.Literal("unknown") })]);
11334
+ /**
11335
+ * A compaction of the session's context during a turn: whether it was `auto` or `manual`, where
11336
+ * the harness says, as Claude Code does and Codex doesn't, and how long after ref0 started the
11337
+ * turn it happened, by the harness's own transcript, where both are known.
11338
+ */
11339
+ const Compaction = Schema.Struct({
11340
+ trigger: Schema.optionalKey(Schema.Literals(["auto", "manual"])),
11341
+ afterMs: Schema.optionalKey(Millis)
11342
+ });
11343
+ /**
11344
+ * A turn's context, read from the session's own transcript as its tokens are: `peak`, the most
11345
+ * one request of the session's own sent, its cached tokens included, none for a turn with no
11346
+ * request, and each compaction. `unknown`, with neither, where the transcript wasn't read whole.
11347
+ * A subagent's context is its own, and isn't in it.
11348
+ */
11349
+ const TurnContext = Schema.Union([Schema.Struct({
11350
+ known: Schema.Literal("read"),
11351
+ peak: Schema.optionalKey(TokenCount),
11352
+ compactions: Schema.Array(Compaction).check(Schema.isMaxLength(100))
11353
+ }), Schema.Struct({ known: Schema.Literal("unknown") })]);
11035
11354
  /**
11036
- * A turn's facts but its tokens, which `TurnFacts` adds for whoever sends them and `turn.ended`
11037
- * carries only to those who may see them.
11355
+ * One of the harness's own subagents that a turn ran, such as Claude Code's Task agents or
11356
+ * Codex's: its type, where the harness names one, and its own tokens, which the turn's tokens
11357
+ * already include, so the turn's less its subagents' are the session's own. Its tokens, and the
11358
+ * turn's, are `unknown` when the harness kept no transcript of it. Not a child session: ref0
11359
+ * knows it only from its `SubagentStop` and its transcript.
11360
+ */
11361
+ const TurnSubagent = Schema.Struct({
11362
+ type: Schema.optionalKey(Name),
11363
+ tokens: TurnTokens
11364
+ });
11365
+ /**
11366
+ * A turn's facts but its tokens and its subagents', which `TurnFacts` adds for whoever sends them
11367
+ * and `turn.ended` carries only to those who may see them.
11038
11368
  */
11039
11369
  const turnFactsFields = {
11040
11370
  /** The turn's model, where the harness said it: Codex in every hook. */
11041
- model: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 128))),
11371
+ model: Schema.optionalKey(Name),
11042
11372
  modelSource: ModelSource,
11043
11373
  /** Tool calls by tool name. */
11044
- tools: Schema.Record(Schema.String.check(Schema.isBetweenLength(1, 128)), Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
11374
+ tools: Schema.Record(Name, Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
11045
11375
  /** The files its edits touched, by repo and path, at most 50; `more` counts the rest. */
11046
11376
  addresses: Schema.Array(Schema.Struct({
11047
11377
  repo: RepoName,
11048
11378
  path: RepoPath
11049
11379
  })).check(Schema.isMaxLength(50)),
11050
- more: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
11380
+ more: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
11381
+ toolTime: ToolTime,
11382
+ context: TurnContext,
11383
+ /** How many subagents past the first 100 the turn ran. */
11384
+ moreSubagents: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
11051
11385
  };
11052
11386
  /**
11053
11387
  * What `ref0 mcp` saw of a turn, sent at its `Stop`s and when the next one starts, and again as
@@ -11059,16 +11393,23 @@ const turnFactsFields = {
11059
11393
  const TurnFacts = Schema.Struct({
11060
11394
  ...turnFactsFields,
11061
11395
  tokens: TurnTokens,
11062
- stoppedAfterMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
11396
+ /** The turn's subagents, in the order their transcripts were first read, at most 100. */
11397
+ subagents: Schema.Array(TurnSubagent).check(Schema.isMaxLength(100)),
11398
+ stoppedAfterMs: Schema.optionalKey(Millis)
11063
11399
  });
11064
11400
  /**
11065
- * A turn's facts as `turn.ended` shows them: its tokens only to an admin of the project's owner
11066
- * and to the session's accountable person. A thread's totals still go to every reader of it, in
11067
- * `ThreadUsage`, so on a thread with one person's sessions they are that person's usage.
11401
+ * A turn's facts as `turn.ended` shows them: its tokens and its subagents' only to an admin of
11402
+ * the project's owner and to the session's accountable person. A thread's totals still go to
11403
+ * every reader of it, in `ThreadUsage`, so on a thread with one person's sessions they are that
11404
+ * person's usage.
11068
11405
  */
11069
11406
  const EndedTurnFacts = Schema.Struct({
11070
11407
  ...turnFactsFields,
11071
- tokens: Schema.optionalKey(TurnTokens)
11408
+ tokens: Schema.optionalKey(TurnTokens),
11409
+ subagents: Schema.Array(Schema.Struct({
11410
+ type: TurnSubagent.fields.type,
11411
+ tokens: Schema.optionalKey(TurnTokens)
11412
+ })).check(Schema.isMaxLength(100))
11072
11413
  });
11073
11414
  /**
11074
11415
  * `POST /v1/sessions/{session}/turns`: a new turn, for the harness's own turn ID, so a retry, or
@@ -11696,151 +12037,6 @@ Schema.Struct({
11696
12037
  name: RepoName
11697
12038
  }))
11698
12039
  });
11699
- /** Where an agent's session asks for an identity token. */
11700
- const identityTokenPath = "/v1/me/identity-token";
11701
- /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
11702
- const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
11703
- /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
11704
- const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
11705
- /**
11706
- * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
11707
- * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
11708
- * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
11709
- */
11710
- const parsesAsWritten = (audience) => {
11711
- const parts = audiencePattern.exec(audience);
11712
- if (parts === null || !URL.canParse(audience)) return false;
11713
- const url = new URL(audience);
11714
- const port = parts[2] ?? "";
11715
- return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
11716
- };
11717
- /**
11718
- * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
11719
- * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
11720
- * case and any port too, so `https://verifier.example:443` is another audience than
11721
- * `https://verifier.example`. ref0 never fetches or resolves it.
11722
- */
11723
- const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
11724
- /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
11725
- const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
11726
- /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
11727
- const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
11728
- /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
11729
- const IssuedIdentityToken = Schema.Struct({
11730
- token: IdentityToken,
11731
- expiresAt: Schema.DateTimeUtcFromMillis
11732
- });
11733
- /**
11734
- * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
11735
- * token, naming its issuance in ref0's log without naming any row or secret.
11736
- */
11737
- const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
11738
- /**
11739
- * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
11740
- * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
11741
- * audience, different at every other, and carrying no time.
11742
- */
11743
- const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
11744
- /** A session, as one audience knows it, made as a person's is. */
11745
- const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
11746
- /** The accountable person, last in a token's chain: only their pairwise ID. */
11747
- const PersonActor = Schema.Struct({ sub: PairwisePersonId });
11748
- /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
11749
- const sessionActing = (act) => Schema.Struct({
11750
- sub: PairwiseSessionId,
11751
- act
11752
- });
11753
- const oneSession = sessionActing(PersonActor);
11754
- const twoSessions = sessionActing(oneSession);
11755
- const threeSessions = sessionActing(twoSessions);
11756
- /**
11757
- * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
11758
- * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
11759
- * allows the agent in `sub`, never an actor.
11760
- */
11761
- const IdentityActor = Schema.Union([
11762
- oneSession,
11763
- twoSessions,
11764
- threeSessions,
11765
- sessionActing(threeSessions)
11766
- ]);
11767
- /** A time in a token, in whole UTC epoch seconds. */
11768
- const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
11769
- Schema.Struct({
11770
- iss: ApiOrigin,
11771
- sub: AgentId,
11772
- aud: IdentityAudience,
11773
- iat: EpochSeconds,
11774
- exp: EpochSeconds,
11775
- jti: IdentityTokenId,
11776
- act: IdentityActor
11777
- }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
11778
- /**
11779
- * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
11780
- * never names two keys.
11781
- */
11782
- const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
11783
- Schema.Struct({
11784
- alg: Schema.Literal("ES256"),
11785
- kid: SigningKeyId,
11786
- typ: Schema.Literal("JWT")
11787
- });
11788
- /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
11789
- const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
11790
- /** One public key in the JWKS: exactly these fields, and never a private one. */
11791
- const PublicSigningKey = Schema.Struct({
11792
- kty: Schema.Literal("EC"),
11793
- crv: Schema.Literal("P-256"),
11794
- x: Coordinate,
11795
- y: Coordinate,
11796
- use: Schema.Literal("sig"),
11797
- alg: Schema.Literal("ES256"),
11798
- kid: SigningKeyId
11799
- });
11800
- Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
11801
- Schema.Struct({
11802
- issuer: ApiOrigin,
11803
- jwks_uri: Schema.String,
11804
- subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
11805
- id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
11806
- claims_supported: Schema.Tuple([
11807
- Schema.Literal("iss"),
11808
- Schema.Literal("sub"),
11809
- Schema.Literal("aud"),
11810
- Schema.Literal("exp"),
11811
- Schema.Literal("iat"),
11812
- Schema.Literal("jti"),
11813
- Schema.Literal("act")
11814
- ])
11815
- });
11816
- /**
11817
- * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
11818
- * `IdentityAudience` says. The refused text is never echoed.
11819
- */
11820
- var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
11821
- /**
11822
- * Less than a whole second is left before the session, a parent or the credential expires, or
11823
- * the token would expire before it's recorded: the next prompt renews or replaces the session.
11824
- */
11825
- var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
11826
- /**
11827
- * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
11828
- * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
11829
- * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
11830
- * received or used it. A failed append discards the token, so every token has its event.
11831
- */
11832
- const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
11833
- session: SessionId,
11834
- agent: AgentRef,
11835
- /** The person whose attachment authorized the session's chain, last in the token's `act`. */
11836
- accountable: PersonId,
11837
- jti: IdentityTokenId,
11838
- audience: IdentityAudience,
11839
- issuer: ApiOrigin,
11840
- keyId: SigningKeyId,
11841
- issuedAt: Schema.DateTimeUtcFromMillis,
11842
- expiresAt: Schema.DateTimeUtcFromMillis
11843
- }));
11844
12040
  Schema.Literals([
11845
12041
  "info/refs",
11846
12042
  "git-upload-pack",
@@ -11868,6 +12064,11 @@ Schema.Union([
11868
12064
  Schema.Struct({
11869
12065
  reader: Reader,
11870
12066
  name: RepoName,
12067
+ /**
12068
+ * For a fork's path, the scope it names: a project, by its ID or its name, or, with none, the
12069
+ * owner's own scope, which holds an organization's org threads.
12070
+ */
12071
+ scope: Schema.optionalKey(Schema.Struct({ project: Schema.optionalKey(ProjectRef) })),
11871
12072
  push: Schema.optionalKey(Schema.Struct({
11872
12073
  thread: ThreadId,
11873
12074
  session: SessionId
@@ -11881,7 +12082,9 @@ const GitTarget = Schema.Struct({
11881
12082
  * their sessions' forks, and the repo's `main`. Unset for one that reads the repo's project as
11882
12083
  * a whole.
11883
12084
  */
11884
- threads: Schema.optionalKey(Schema.Array(ThreadId))
12085
+ threads: Schema.optionalKey(Schema.Array(ThreadId)),
12086
+ /** For a fork's path, the project its scope resolved to: a project's, or the owner's own. */
12087
+ scope: Schema.optionalKey(ProjectId)
11885
12088
  });
11886
12089
  Schema.Union([GitTarget, RepoNotFound]);
11887
12090
  /** A repo whose baseline isn't readable yet: Git is told to try again in a few seconds. */
@@ -11892,28 +12095,6 @@ const CurrentBaseline = Schema.Struct({
11892
12095
  name: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/))
11893
12096
  });
11894
12097
  Schema.Union([CurrentBaseline, RepoNotReady]);
11895
- //#endregion
11896
- //#region ../contracts/src/fork-calls.ts
11897
- /**
11898
- * A session's fork of a repo: its own copy, made in the background from the repo's current
11899
- * baseline when the session first reaches the repo. Agents push only to their own session's
11900
- * forks. A fork isn't deleted yet: when it may be, and what's saved first, waits for the user.
11901
- */
11902
- /**
11903
- * Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
11904
- * project's visibility, which a session's routes read who sees the project by.
11905
- */
11906
- const SessionPlace = Schema.Struct({
11907
- owner: OwnerId,
11908
- project: ProjectId,
11909
- visibility: Visibility,
11910
- thread: ThreadId,
11911
- threadNumber: ThreadNumber,
11912
- session: SessionId,
11913
- number: SessionNumber,
11914
- /** Whether it's still going: not ended, and its lease not run out. */
11915
- live: Schema.Boolean
11916
- });
11917
12098
  Schema.Struct({ session: SessionId });
11918
12099
  Schema.Union([SessionPlace, SessionNotFound]);
11919
12100
  Schema.Struct({
@@ -14579,7 +14760,10 @@ const HistoryPack = Schema.Struct({
14579
14760
  first: MergeSequence,
14580
14761
  last: MergeSequence
14581
14762
  })),
14582
- /** A baseline or deepen pack: the baseline it belongs to. After a rebuild, a retired baseline's sessions still deepen from its own chain. */
14763
+ /**
14764
+ * A baseline, deepen or rolled-up pack: the baseline it belongs to. After a rebuild, a retired
14765
+ * baseline's sessions still deepen from its own chain, and its rollups hold nothing of the new one's.
14766
+ */
14583
14767
  baseline: Schema.optionalKey(GitRepositoryId),
14584
14768
  /** A deepen pack: how deep its baseline reaches with it and every pack before it. */
14585
14769
  depth: Schema.optionalKey(Depth),
@@ -15208,23 +15392,30 @@ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
15208
15392
  generation: AttachedSoFar
15209
15393
  })]);
15210
15394
  Context.Service()("ref0/contracts/Caller");
15395
+ Context.Service()("ref0/contracts/ProvedKey");
15211
15396
  /** A browser session's cookie, as `/v1` reads it. */
15212
15397
  const browserSession = HttpApiSecurity.apiKey({
15213
15398
  in: "cookie",
15214
15399
  key: browserSessionCookie
15215
15400
  });
15216
15401
  /**
15217
- * Every endpoint's credential, checked before its handler runs: a bearer token, or a browser
15218
- * session's cookie. A request with both is refused before it gets here.
15402
+ * Every endpoint's credential, checked before its handler runs: a bearer token, a token bound to
15403
+ * a key with a proof that key signed (`Authorization: DPoP` and one `DPoP` header), or a browser
15404
+ * session's cookie. A request with a cookie and a token is refused before it gets here. Its
15405
+ * handler learns who's acting, and the key its proof was signed by, if it came with one, which a
15406
+ * session's start binds the session to. A client that signs proofs may fail before it sends
15407
+ * anything, when its key store doesn't sign.
15219
15408
  */
15220
15409
  var Authorization = class extends HttpApiMiddleware.Service()("ref0/contracts/Authorization", {
15221
15410
  requiredForClient: true,
15222
15411
  security: {
15223
15412
  bearer: HttpApiSecurity.bearer,
15413
+ dpop: HttpApiSecurity.http({ scheme: "DPoP" }),
15224
15414
  browserSession
15225
15415
  },
15226
15416
  error: [
15227
15417
  Unauthenticated,
15418
+ InvalidDpopProof,
15228
15419
  SessionHasEnded,
15229
15420
  AuthorityUnavailable
15230
15421
  ]
@@ -15256,6 +15447,17 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
15256
15447
  */
15257
15448
  var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
15258
15449
  /**
15450
+ * A request with no credential, signed by a key it names, such as a machine's ask to connect: one
15451
+ * `DPoP` header, a proof with no `ath`, since there's no token, checked before the request is
15452
+ * read, which says the key that signed it; or no proof, which says none. Every client supplies
15453
+ * it, as it does `Authorization`, so its key store's failure, before anything is sent, is typed:
15454
+ * one with a key store signs, and every other sends no proof.
15455
+ */
15456
+ var KeyProof = class extends HttpApiMiddleware.Service()("ref0/contracts/KeyProof", {
15457
+ requiredForClient: true,
15458
+ error: InvalidDpopProof
15459
+ }) {};
15460
+ /**
15259
15461
  * An identity token's request whose audience its contract refuses, missing or not an HTTPS
15260
15462
  * origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
15261
15463
  */
@@ -15370,8 +15572,9 @@ var AttachmentsGroup = class extends HttpApiGroup.make("attachments").add(HttpAp
15370
15572
  })).middleware(Authorization) {};
15371
15573
  /**
15372
15574
  * Connecting a machine, the machine's side: asking, with no credential, to connect to one owner's
15373
- * agent, whose terminal shows the code their person types; then collecting what came of it,
15374
- * confirming it saved the grant, or aborting, with its collecting secret.
15575
+ * agent, whose terminal shows the code their person types, signed by the machine key it names;
15576
+ * then collecting what came of it, confirming it saved the grant, or aborting, with its collecting
15577
+ * secret, which needs no proof: a grant it collects is bound to the key its ask named.
15375
15578
  */
15376
15579
  var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachments").add(HttpApiEndpoint.post("request", "/v1/attachments/requests", {
15377
15580
  headers: idempotency,
@@ -15381,9 +15584,10 @@ var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachment
15381
15584
  NotPermitted,
15382
15585
  CommandRefused,
15383
15586
  SlowDown,
15587
+ InvalidDpopProof,
15384
15588
  AuthorityUnavailable
15385
15589
  ]
15386
- }).middleware(MachineNameRefusal), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15590
+ }).middleware(MachineNameRefusal).middleware(KeyProof), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15387
15591
  params: { approval: ApprovalId },
15388
15592
  payload: Schema.Struct({ answerCode: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(64))) }),
15389
15593
  success: RequestState,
@@ -18857,6 +19061,8 @@ const HookArguments = Schema.Struct({
18857
19061
  tool_input: ToolValue,
18858
19062
  tool_response: ToolValue,
18859
19063
  error: Field,
19064
+ /** How long the tool ran, in milliseconds, at Claude Code's `PostToolUse` and its failure. */
19065
+ duration_ms: Field,
18860
19066
  /** A string from Claude Code, a boolean from Codex, which keeps a placeholder's JSON type. */
18861
19067
  stop_hook_active: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.String])),
18862
19068
  /** Codex names the turn's model in every hook but `SessionEnd`. */
@@ -19001,14 +19207,146 @@ const projectPage = (page, project) => page.replace("/$owner/$project", `/${proj
19001
19207
  * credential it sends, a bearer token or a browser session.
19002
19208
  */
19003
19209
  const ref0Client = (origin) => HttpApiClient.make(Api, origin === void 0 ? {} : { baseUrl: origin });
19210
+ /** No proof of a key for a request that asks for one: a client with no key store's. */
19211
+ const noKeyProof = HttpApiMiddleware.layerClient(KeyProof, ({ request, next }) => next(request));
19004
19212
  /** A bearer token: an agent's credential, sent with every request. */
19005
- const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19213
+ const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19006
19214
  /** No credential: a terminal asking its person, which sends nothing of theirs. */
19007
- const anonymousCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19215
+ const anonymousCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19008
19216
  FetchHttpClient.layer;
19009
19217
  //#endregion
19218
+ //#region ../sdk/src/client/proofs.ts
19219
+ /**
19220
+ * How far ref0's clock is ahead of this machine's, in milliseconds, as the `Date` of a refused
19221
+ * proof's answer said: provided once for a process, so every proof it signs after is dated by it.
19222
+ */
19223
+ var ProofClock = class ProofClock extends Context.Service()("ref0/sdk/ProofClock") {
19224
+ static layer = Layer.effect(ProofClock, Effect.map(Ref.make(0), (offset) => ProofClock.of({ offset })));
19225
+ };
19226
+ /** How far apart the two clocks may be before a refused proof is signed again by ref0's. */
19227
+ const clockSkewMillis = 5e3;
19228
+ const encodeHeader = Schema.encodeSync(Schema.fromJsonString(DpopProofHeader));
19229
+ const encodeClaims = Schema.encodeSync(Schema.fromJsonString(DpopProofClaims));
19230
+ const decodeProofId = Schema.decodeSync(DpopProofId);
19231
+ const utf8 = new TextEncoder();
19232
+ /** A fresh UUIDv4 from the platform's cryptographic source, a proof's `jti`. */
19233
+ const freshProofId = Effect.sync(() => {
19234
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
19235
+ bytes[6] = 64 | (bytes[6] ?? 0) & 15;
19236
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
19237
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
19238
+ return decodeProofId(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`);
19239
+ });
19240
+ const decodeThumbprint = Schema.decodeSync(KeyThumbprint);
19241
+ /**
19242
+ * RFC 7638's thumbprint of a P-256 public key: the SHA-256 of its required members, in order, as
19243
+ * JSON, in base64url. What a credential bound to the key names it by, as `cnf.jkt`.
19244
+ */
19245
+ const keyThumbprint = (jwk) => Effect.promise(async () => decodeThumbprint(Base64Url.encode(new Uint8Array(await crypto.subtle.digest("SHA-256", utf8.encode(`{"crv":"P-256","kty":"EC","x":"${jwk.x}","y":"${jwk.y}"}`))))));
19246
+ /** `ath`: the SHA-256 of the whole token, in base64url. */
19247
+ const tokenHashOf = (token) => Effect.promise(async () => Base64Url.encode(new Uint8Array(await crypto.subtle.digest("SHA-256", utf8.encode(token)))));
19248
+ /**
19249
+ * A proof for one attempt at a request: its method, and its URL as built, with no query or
19250
+ * fragment; dated by this machine's clock, moved by `offset`; with the token's hash, if it's sent
19251
+ * with one; signed by the store.
19252
+ */
19253
+ const proofFor = Effect.fnUntraced(function* (store, attempt, offset) {
19254
+ const now = (yield* Clock.currentTimeMillis) + offset;
19255
+ const header = encodeHeader({
19256
+ typ: "dpop+jwt",
19257
+ alg: "ES256",
19258
+ jwk: store.publicKey
19259
+ });
19260
+ const signs = {
19261
+ jti: yield* freshProofId,
19262
+ htm: attempt.method,
19263
+ htu: `${attempt.url.origin}${attempt.url.pathname}`,
19264
+ iat: Math.floor(now / 1e3)
19265
+ };
19266
+ if (attempt.token !== void 0) signs.ath = yield* tokenHashOf(attempt.token);
19267
+ const claims = encodeClaims(signs);
19268
+ const signed = `${Base64Url.encode(header)}.${Base64Url.encode(claims)}`;
19269
+ const signature = yield* store.sign(utf8.encode(signed));
19270
+ if (signature.byteLength !== 64) return yield* Effect.die("a key store answers r||s, 64 bytes");
19271
+ return `${signed}.${Base64Url.encode(signature)}`;
19272
+ });
19273
+ /**
19274
+ * How far ref0's clock was from this machine's, moved by `offset`, by a refused proof's answer:
19275
+ * none for any other answer, or one without a `Date`.
19276
+ */
19277
+ const skewOf = (response, local) => {
19278
+ const challenge = response.headers["www-authenticate"] ?? "";
19279
+ if (response.status !== 401 || !challenge.includes("DPoP error=\"invalid_dpop_proof\"")) return Option.none();
19280
+ return Option.map(DateTime.make(response.headers["date"] ?? ""), (date) => DateTime.toEpochMillis(date) - local);
19281
+ };
19282
+ /**
19283
+ * Sends one request, with its credential if it has one, signing each attempt afresh: once, and
19284
+ * once more, by ref0's clock, after a refused proof whose answer is dated more than five seconds
19285
+ * from this machine's. The process keeps the offset for every proof after.
19286
+ */
19287
+ const signedAttempts = (credential, offsetRef) => ({ request, next }) => Effect.gen(function* () {
19288
+ const token = credential.token === void 0 ? void 0 : Redacted.value(credential.token);
19289
+ const attempt = {
19290
+ method: request.method,
19291
+ url: new URL(`${credential.origin}${request.url}`),
19292
+ token
19293
+ };
19294
+ const redacted = [...yield* Effect.service(Headers.CurrentRedactedNames), "dpop"];
19295
+ const send = (offset) => proofFor(credential.store, attempt, offset).pipe(Effect.map((proof) => HttpClientRequest.setHeaders(request, token === void 0 ? { dpop: proof } : {
19296
+ authorization: `DPoP ${token}`,
19297
+ dpop: proof
19298
+ })), Effect.flatMap(next), Effect.provideService(Headers.CurrentRedactedNames, redacted));
19299
+ const offset = yield* Ref.get(offsetRef);
19300
+ const answer = yield* send(offset);
19301
+ const skew = skewOf(answer, (yield* Clock.currentTimeMillis) + offset);
19302
+ if (Option.isNone(skew) || Math.abs(skew.value) <= clockSkewMillis) return answer;
19303
+ yield* Effect.ignore(answer.text);
19304
+ yield* Ref.set(offsetRef, offset + skew.value);
19305
+ return yield* send(offset + skew.value);
19306
+ });
19307
+ /**
19308
+ * A token bound to a key, sent as `Authorization: DPoP` with a proof its key store signs afresh
19309
+ * for each attempt. Signing in's own endpoints take no token.
19310
+ */
19311
+ const dpopCredential = (credential) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, Effect.map(Effect.service(ProofClock), (clock) => signedAttempts(credential, clock.offset))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19312
+ /**
19313
+ * ref0's `/v1` as a typed client at a bound credential's origin, sending its token with a proof
19314
+ * its key store signs for each attempt. The client is made at the origin its proofs name, so
19315
+ * neither the token nor a proof is sent anywhere else. It needs the process's `ProofClock`. A
19316
+ * request's trace redacts its proof, but an error it fails with still holds the request: a
19317
+ * program that logs errors adds `dpop` to `Headers.CurrentRedactedNames` at its root.
19318
+ */
19319
+ const boundClient = (credential) => ref0Client(credential.origin).pipe(Effect.provide(dpopCredential(credential)));
19320
+ /**
19321
+ * A bound credential's requests made by hand, each with a path under its origin, such as a
19322
+ * command built from its endpoint's route: sent there with its token and a proof its key store
19323
+ * signs afresh for each attempt, as `boundClient` sends its own, and never anywhere else. It needs
19324
+ * the process's `ProofClock`, and its errors hold their requests, as `boundClient`'s do.
19325
+ */
19326
+ const boundSend = (credential) => Effect.gen(function* () {
19327
+ const http = yield* HttpClient.HttpClient;
19328
+ const clock = yield* ProofClock;
19329
+ const attempts = signedAttempts(credential, clock.offset);
19330
+ return (request) => attempts({
19331
+ request,
19332
+ next: (signed) => http.execute(HttpClientRequest.prependUrl(signed, credential.origin))
19333
+ });
19334
+ });
19335
+ /**
19336
+ * A request with no credential, signed by the key it names, such as a machine's ask to connect:
19337
+ * a proof its key store signs for each attempt, with no `ath`, since there's no token. Any other
19338
+ * request is sent with nothing added.
19339
+ */
19340
+ const keyProof = (signer) => Layer.mergeAll(HttpApiMiddleware.layerClient(KeyProof, Effect.map(Effect.service(ProofClock), (clock) => signedAttempts(signer, clock.offset))), HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19341
+ /**
19342
+ * ref0's `/v1` as a typed client at a key's origin, with no credential, signing each request that
19343
+ * asks for a proof, such as a machine's ask to connect, with the key its store holds. It needs the
19344
+ * process's `ProofClock`, and its errors still hold their requests, as `boundClient`'s do.
19345
+ */
19346
+ const provingClient = (signer) => ref0Client(signer.origin).pipe(Effect.provide(keyProof(signer)));
19347
+ //#endregion
19010
19348
  //#region ../sdk/src/client/request-id.ts
19011
- const decodeRequestId$1 = Schema.decodeUnknownEffect(RequestId);
19349
+ const decodeRequestId = Schema.decodeUnknownEffect(RequestId);
19012
19350
  /**
19013
19351
  * A new delivery ID, for a request that changes something's `Idempotency-Key`: a UUIDv7 from the
19014
19352
  * clock and ten random bytes, so a retry sends the same one and gets the first answer.
@@ -19017,7 +19355,7 @@ const newRequestId = Effect.gen(function* () {
19017
19355
  const millis = yield* Clock.currentTimeMillis;
19018
19356
  const random = /* @__PURE__ */ new Uint8Array(10);
19019
19357
  for (let index = 0; index < random.length; index += 1) random[index] = yield* Random.nextIntBetween(0, 255);
19020
- return yield* Effect.orDie(decodeRequestId$1(`request_${encodeSuffix(uuidV7(millis, random))}`));
19358
+ return yield* Effect.orDie(decodeRequestId(`request_${encodeSuffix(uuidV7(millis, random))}`));
19021
19359
  });
19022
19360
  //#endregion
19023
19361
  //#region src/sha256.ts
@@ -19088,6 +19426,317 @@ var Ref0Config = class Ref0Config extends Context.Service()("ref0/cli/Ref0Config
19088
19426
  })).pipe(Layer.provide(NodeServices.layer));
19089
19427
  };
19090
19428
  //#endregion
19429
+ //#region src/keychain.ts
19430
+ /**
19431
+ * The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
19432
+ * makes, and a session's token. Never a file. On macOS through `security`, on Linux through
19433
+ * `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
19434
+ * an argument, since any process on the machine can read another's arguments.
19435
+ */
19436
+ var KeychainError = class extends Schema.TaggedError()("KeychainError", {
19437
+ nextStep: Schema.String,
19438
+ /** The keychain answered, with an item ref0 can't read, rather than not answering. */
19439
+ unreadable: Schema.optionalKey(Schema.Literal(true))
19440
+ }) {};
19441
+ /**
19442
+ * The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
19443
+ * grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
19444
+ */
19445
+ const keychainIn = Effect.fnUntraced(function* (service) {
19446
+ const crypto = yield* Crypto.Crypto;
19447
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
19448
+ const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
19449
+ const unavailable = new KeychainError({ nextStep: macOS ? "The keychain didn’t answer. Unlock it, and try again." : "The keychain didn’t answer. Unlock the keychain or start a secret service, and try again." });
19450
+ const missing = new KeychainError({ nextStep: "The keychain didn’t answer: ref0 needs `secret-tool`, from libsecret. Install `secret-tool` with `sudo apt install libsecret-tools` on Debian or Ubuntu, `sudo dnf install libsecret` on Fedora or `sudo pacman -S libsecret` on Arch." });
19451
+ const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
19452
+ const handle = yield* spawner.spawn(ChildProcess.make(command, args, { stdin: Stream.make(new TextEncoder().encode(stdin)) })).pipe(Effect.mapError((error) => !macOS && Predicate.isTagged(error.reason, "NotFound") ? missing : unavailable));
19453
+ const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
19454
+ const code = yield* handle.exitCode;
19455
+ return {
19456
+ output: output.trim(),
19457
+ complaint: complaint.trim(),
19458
+ code,
19459
+ succeeded: code === ChildProcessSpawner.ExitCode(0)
19460
+ };
19461
+ })).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
19462
+ const get = Effect.fn("Keychain.get")(function* (account) {
19463
+ const found = macOS ? yield* run("security", [
19464
+ "find-generic-password",
19465
+ "-s",
19466
+ service,
19467
+ "-a",
19468
+ account,
19469
+ "-w"
19470
+ ], "") : yield* run("secret-tool", [
19471
+ "lookup",
19472
+ "service",
19473
+ service,
19474
+ "account",
19475
+ account
19476
+ ], "");
19477
+ if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
19478
+ if (macOS) {
19479
+ if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
19480
+ return yield* unavailable;
19481
+ }
19482
+ if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
19483
+ const listed = yield* run("secret-tool", [
19484
+ "search",
19485
+ "--all",
19486
+ "service",
19487
+ service,
19488
+ "account",
19489
+ account
19490
+ ], "");
19491
+ if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
19492
+ return Option.none();
19493
+ });
19494
+ const set = Effect.fn("Keychain.set")(function* (account, secret) {
19495
+ const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
19496
+ const value = Redacted.value(secret);
19497
+ if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
19498
+ const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
19499
+ if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
19500
+ if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
19501
+ "store",
19502
+ "--label",
19503
+ `ref0 ${account}`,
19504
+ "service",
19505
+ service,
19506
+ "account",
19507
+ account
19508
+ ], Redacted.value(secret))).succeeded) return yield* unavailable;
19509
+ });
19510
+ const remove = Effect.fn("Keychain.remove")(function* (account) {
19511
+ if (!(yield* macOS ? run("security", [
19512
+ "delete-generic-password",
19513
+ "-s",
19514
+ service,
19515
+ "-a",
19516
+ account
19517
+ ], "") : run("secret-tool", [
19518
+ "clear",
19519
+ "service",
19520
+ service,
19521
+ "account",
19522
+ account
19523
+ ], "")).succeeded) return yield* unavailable;
19524
+ });
19525
+ const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
19526
+ return {
19527
+ probe: Effect.gen(function* () {
19528
+ const account = `connect-probe/${yield* random}`;
19529
+ const value = Redacted.make(yield* random);
19530
+ yield* Effect.gen(function* () {
19531
+ yield* set(account, value);
19532
+ const kept = yield* get(account);
19533
+ if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
19534
+ yield* remove(account);
19535
+ }).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
19536
+ }).pipe(Effect.timeoutOrElse({
19537
+ duration: Duration.minutes(1),
19538
+ orElse: () => Effect.fail(unavailable)
19539
+ })),
19540
+ get,
19541
+ set,
19542
+ remove
19543
+ };
19544
+ });
19545
+ var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
19546
+ static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
19547
+ };
19548
+ //#endregion
19549
+ //#region src/machine-key.ts
19550
+ /**
19551
+ * This machine's key, which signs a proof for its ask to connect and for each request a host
19552
+ * grant makes: one ES256 key for each OS account and origin, in the origin's keychain service
19553
+ * under account `machine-key`, as `{ store: 'software', jwk }`. `ref0 connect` makes it, under its
19554
+ * lock, only when the keychain says it holds none, and nothing else makes one. A process reads it
19555
+ * once and keeps it, since each read is a `security` or `secret-tool` process. Its private half
19556
+ * goes to the keychain on stdin and into a Web Crypto key that can't be exported, and nowhere
19557
+ * else: no argument, environment, config, error or log.
19558
+ */
19559
+ /** The keychain account the key is kept under. */
19560
+ const account = "machine-key";
19561
+ /** How long reading and importing the key may take for a request, and signing with it. */
19562
+ const deadline = Duration.seconds(3);
19563
+ /**
19564
+ * How long `ref0 connect` may wait on the keychain to read, make and keep the key, as its probe
19565
+ * may: a prompt to unlock the keychain can take that long.
19566
+ */
19567
+ const connectDeadline = Duration.minutes(1);
19568
+ /** A P-256 coordinate or private scalar: 32 bytes, in base64url. */
19569
+ const Scalar = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/));
19570
+ /** A P-256 private key as a JWK: its public point, and its private scalar `d`. */
19571
+ const PrivateJwk = Schema.Struct({
19572
+ kty: Schema.Literal("EC"),
19573
+ crv: Schema.Literal("P-256"),
19574
+ x: Scalar,
19575
+ y: Scalar,
19576
+ d: Scalar
19577
+ });
19578
+ /** The keychain's record: which store keeps the key, and the key. A hardware store keeps its own. */
19579
+ const KeptKey = Schema.Struct({
19580
+ store: Schema.Literal("software"),
19581
+ jwk: PrivateJwk
19582
+ });
19583
+ const decodeKept$3 = Schema.decodeUnknownOption(Schema.fromJsonString(KeptKey));
19584
+ const encodeKept$3 = Schema.encodeSync(Schema.fromJsonString(KeptKey));
19585
+ const decodePrivateJwk = Schema.decodeUnknownEffect(PrivateJwk);
19586
+ const decodePublicJwk = Schema.decodeSync(PublicJwk);
19587
+ const ecdsa = {
19588
+ name: "ECDSA",
19589
+ namedCurve: "P-256"
19590
+ };
19591
+ const es256 = {
19592
+ name: "ECDSA",
19593
+ hash: "SHA-256"
19594
+ };
19595
+ /** What a key signs to prove it signs for its public half. */
19596
+ const proved = new TextEncoder().encode("ref0 machine key");
19597
+ const timedOut = new KeychainError({ nextStep: "The keychain didn’t answer in time. Check that it’s unlocked, and try again." });
19598
+ const unreadable$1 = new KeychainError({ nextStep: "ref0 can’t read this machine’s key in the keychain, so it signs nothing and makes no other. Update ref0, which may have kept it in a newer form, or repair the keychain, and try again." });
19599
+ const notKept = new KeychainError({ nextStep: "The keychain didn’t keep this machine’s key. Check that it’s unlocked, then run ref0 connect again." });
19600
+ const windows = new KeychainError({ nextStep: "ref0 connect doesn’t run on Windows yet: it keeps this machine’s key in the macOS keychain or a Linux secret service. Run it in WSL instead." });
19601
+ const didNotSign = new KeyStoreUnavailable({ nextStep: "This machine’s key didn’t sign in time. Try again." });
19602
+ /** A fresh key's private JWK, with only the members the record keeps. */
19603
+ const freshKey = Effect.promise(async () => {
19604
+ const pair = await crypto.subtle.generateKey(ecdsa, true, ["sign", "verify"]);
19605
+ return await crypto.subtle.exportKey("jwk", pair.privateKey);
19606
+ }).pipe(Effect.flatMap(({ kty, crv, x, y, d }) => decodePrivateJwk({
19607
+ kty,
19608
+ crv,
19609
+ x,
19610
+ y,
19611
+ d
19612
+ })), Effect.orDie);
19613
+ /**
19614
+ * A key store over a private JWK: imported once, as a key that can't be exported, and proved to
19615
+ * sign for its public half, or it's no key to bind anything to.
19616
+ */
19617
+ const softwareStore = Effect.fnUntraced(function* (jwk) {
19618
+ const publicKey = decodePublicJwk({
19619
+ kty: jwk.kty,
19620
+ crv: jwk.crv,
19621
+ x: jwk.x,
19622
+ y: jwk.y
19623
+ });
19624
+ const signs = yield* Effect.tryPromise({
19625
+ try: async () => {
19626
+ const [signing, verifying] = await Promise.all([crypto.subtle.importKey("jwk", jwk, ecdsa, false, ["sign"]), crypto.subtle.importKey("jwk", publicKey, ecdsa, false, ["verify"])]);
19627
+ const signature = await crypto.subtle.sign(es256, signing, proved);
19628
+ return await crypto.subtle.verify(es256, verifying, signature, proved) ? signing : void 0;
19629
+ },
19630
+ catch: () => unreadable$1
19631
+ });
19632
+ if (signs === void 0) return yield* unreadable$1;
19633
+ const sign = (bytes) => Effect.tryPromise({
19634
+ try: async () => new Uint8Array(await crypto.subtle.sign(es256, signs, new Uint8Array(bytes))),
19635
+ catch: () => didNotSign
19636
+ }).pipe(Effect.timeoutOrElse({
19637
+ duration: deadline,
19638
+ orElse: () => Effect.fail(didNotSign)
19639
+ }));
19640
+ return {
19641
+ store: {
19642
+ publicKey,
19643
+ sign
19644
+ },
19645
+ binding: { jkt: yield* keyThumbprint(publicKey) }
19646
+ };
19647
+ });
19648
+ /**
19649
+ * This machine's key, read once for the process. `supported` refuses a platform with no keychain
19650
+ * that keeps it, such as Windows, before `ref0 connect` asks the keychain anything. `ensure` is
19651
+ * `ref0 connect`'s: the key the keychain holds, or one made and kept now if it says it holds none,
19652
+ * under the connect lock, once the keychain is probed. `boundTo` is for a kept
19653
+ * credential bound to a key: the key it names, read again once if the one held is another, such
19654
+ * as one replaced since, or `InvalidDpopProof`, without sending anything, when the keychain has
19655
+ * none or another. Neither makes a key from an item it can't read.
19656
+ */
19657
+ var MachineKey = class MachineKey extends Context.Service()("ref0/cli/MachineKey") {
19658
+ /** On the platform Node names, such as `win32`, where no keychain keeps the key yet. */
19659
+ static layerOn = (platform) => Layer.effect(MachineKey, Effect.gen(function* () {
19660
+ const keychain = yield* Keychain;
19661
+ const held = yield* Ref.make(Option.none());
19662
+ const reading = yield* Semaphore.make(1);
19663
+ const read = Effect.gen(function* () {
19664
+ const kept = yield* keychain.get(account);
19665
+ if (Option.isNone(kept)) return Option.none();
19666
+ const record = decodeKept$3(Redacted.value(kept.value));
19667
+ if (Option.isNone(record)) return yield* unreadable$1;
19668
+ const key = yield* softwareStore(record.value.jwk);
19669
+ yield* Ref.set(held, Option.some(key));
19670
+ return Option.some(key);
19671
+ });
19672
+ const made = Effect.gen(function* () {
19673
+ const jwk = yield* freshKey;
19674
+ yield* keychain.set(account, Redacted.make(encodeKept$3({
19675
+ store: "software",
19676
+ jwk
19677
+ })));
19678
+ const back = yield* read;
19679
+ const same = Option.exists(back, (key) => key.store.publicKey.x === jwk.x && key.store.publicKey.y === jwk.y);
19680
+ if (Option.isNone(back) || !same) return yield* notKept;
19681
+ return back.value;
19682
+ });
19683
+ const supported = platform === "win32" ? Effect.fail(windows) : Effect.void;
19684
+ const ensure = Effect.gen(function* () {
19685
+ const kept = yield* read;
19686
+ return Option.isSome(kept) ? kept.value : yield* made;
19687
+ }).pipe(Effect.timeoutOrElse({
19688
+ duration: connectDeadline,
19689
+ orElse: () => Effect.fail(timedOut)
19690
+ }), reading.withPermits(1));
19691
+ const boundTo = (binding) => Effect.gen(function* () {
19692
+ const now = yield* Ref.get(held);
19693
+ if (Option.isSome(now) && now.value.binding.jkt === binding.jkt) return now.value.store;
19694
+ const kept = yield* read.pipe(Effect.timeoutOrElse({
19695
+ duration: deadline,
19696
+ orElse: () => Effect.fail(timedOut)
19697
+ }));
19698
+ if (Option.isSome(kept) && kept.value.binding.jkt === binding.jkt) return kept.value.store;
19699
+ return yield* invalidDpopProof;
19700
+ }).pipe(reading.withPermits(1));
19701
+ return MachineKey.of({
19702
+ supported,
19703
+ ensure,
19704
+ boundTo
19705
+ });
19706
+ }));
19707
+ static layer = MachineKey.layerOn(process.platform);
19708
+ };
19709
+ //#endregion
19710
+ //#region src/kept-client.ts
19711
+ const decodeProofOrigin$1 = Schema.decodeUnknownEffect(ProofOrigin);
19712
+ /** A bound credential: its token, at ref0's origin, with the key it's bound to, read once. */
19713
+ const boundTo = Effect.fnUntraced(function* (kept, cnf) {
19714
+ const config = yield* Ref0Config;
19715
+ const store = yield* (yield* MachineKey).boundTo(cnf);
19716
+ return {
19717
+ origin: yield* Effect.orDie(decodeProofOrigin$1(config.api)),
19718
+ store,
19719
+ token: kept.token
19720
+ };
19721
+ });
19722
+ /** ref0's `/v1` as a typed client acting with a kept credential. */
19723
+ const keptClient = Effect.fnUntraced(function* (kept) {
19724
+ if (kept.cnf === void 0) {
19725
+ const config = yield* Ref0Config;
19726
+ return yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(kept.token)));
19727
+ }
19728
+ return yield* boundClient(yield* boundTo(kept, kept.cnf));
19729
+ });
19730
+ /** Requests made by hand, each with a path under ref0's origin, sent with a kept credential. */
19731
+ const keptSend = Effect.fnUntraced(function* (kept) {
19732
+ if (kept.cnf === void 0) {
19733
+ const config = yield* Ref0Config;
19734
+ const http = yield* HttpClient.HttpClient;
19735
+ return (request) => http.execute(HttpClientRequest.prependUrl(HttpClientRequest.bearerToken(request, kept.token), config.api));
19736
+ }
19737
+ return yield* boundSend(yield* boundTo(kept, kept.cnf));
19738
+ });
19739
+ //#endregion
19091
19740
  //#region src/secret.ts
19092
19741
  const hashed = (secret) => Effect.gen(function* () {
19093
19742
  const digest = yield* Effect.promise(async () => await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
@@ -19290,15 +19939,36 @@ const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] :
19290
19939
  //#endregion
19291
19940
  //#region src/session.ts
19292
19941
  /**
19942
+ * A session's credential as this machine keeps it, in one keychain write: its token, its
19943
+ * session, the key it's bound to, if it is, and the host grant its chain started from, by its
19944
+ * ID, so a grant kept since tells whether it's the one that started it.
19945
+ */
19946
+ const KeptSession = Schema.Struct({
19947
+ token: Schema.RedactedFromValue(Schema.String),
19948
+ session: SessionId,
19949
+ /** This machine's key, by its thumbprint, when ref0 bound the session to it. */
19950
+ cnf: Schema.optional(KeyBinding),
19951
+ hostGrant: CredentialRef
19952
+ });
19953
+ const decodeKeptSession = Schema.decodeUnknownOption(Schema.fromJsonString(KeptSession));
19954
+ const encodeKeptSession = Schema.encodeSync(Schema.fromJsonString(KeptSession));
19955
+ /**
19956
+ * A kept session's record, from the keychain's value: none for one this ref0 can't read, such as
19957
+ * the bare token an earlier ref0 kept, which the next prompt replaces with a session of its own.
19958
+ */
19959
+ const readKeptSession = (value) => Option.flatMap(value, (kept) => decodeKeptSession(Redacted.value(kept)));
19960
+ /** A kept session's record, as the keychain keeps it. */
19961
+ const keptSessionValue = (kept) => Redacted.make(encodeKeptSession(kept));
19962
+ /**
19293
19963
  * An agent's command in a harness session that has no ref0 session, though an agent is connected
19294
19964
  * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
19295
19965
  * person instead.
19296
19966
  */
19297
19967
  var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
19298
19968
  /**
19299
- * The token of the session this command runs in, if it runs in one: an agent's, which its
19300
- * harness bound when the session started. Where it's found is the harness's to say. Outside a
19301
- * session, there's none, and every command is the person's, asked by approval.
19969
+ * The session this command runs in, as its credential is kept, if it runs in one: an agent's,
19970
+ * which its harness bound when the session started. Where it's found is the harness's to say.
19971
+ * Outside a session, there's none, and every command is the person's, asked by approval.
19302
19972
  */
19303
19973
  var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
19304
19974
  /** No session: until a harness binds one, the person asks, by approval. */
@@ -19315,9 +19985,11 @@ var SessionThread = class SessionThread extends Context.Service()("ref0/cli/Sess
19315
19985
  //#endregion
19316
19986
  //#region src/call.ts
19317
19987
  /**
19318
- * Every command's one way to ref0: an endpoint's request, sent with the session's token when
19319
- * the command runs in a session, or else asked of the person, by approval, from this terminal.
19320
- * Either way the answer is the endpoint's own, as JSON.
19988
+ * Every command's one way to ref0: an endpoint's request, sent as the session when the command
19989
+ * runs in one, signed with this machine's key when the session's bound to it, or else asked of
19990
+ * the person, by approval, from this terminal. Either way the answer is the endpoint's own, as
19991
+ * JSON. A session's request its key can't sign fails here, as the session's: never asked of the
19992
+ * person instead.
19321
19993
  */
19322
19994
  /** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
19323
19995
  var CallFailed = class extends Schema.TaggedError()("CallFailed", {
@@ -19453,42 +20125,52 @@ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19453
20125
  const config = yield* Ref0Config;
19454
20126
  const session = yield* SessionToken;
19455
20127
  const http = yield* HttpClient.HttpClient;
19456
- const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
20128
+ const signing = yield* Effect.context();
20129
+ const asSession = Effect.fnUntraced(function* (kept, request, endpoint) {
19457
20130
  const timeout = timeoutOf(request.command.endpoint);
19458
- const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
20131
+ const send = yield* keptSend(kept).pipe(Effect.provide(signing), Effect.provideService(Ref0Config, config), Effect.provideService(HttpClient.HttpClient, http), Effect.mapError((error) => new CallFailed({
20132
+ status: 0,
20133
+ kind: error._tag,
20134
+ nextStep: error.nextStep
20135
+ })));
20136
+ const base = HttpClientRequest.make(request.method)(request.url).pipe(HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
19459
20137
  const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
19460
20138
  const change = request.method !== "GET";
19461
20139
  const lost = change ? unanswered$1 : unreachable;
19462
- const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
20140
+ const { response, text } = yield* send(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
19463
20141
  response,
19464
20142
  text
20143
+ }))), Effect.catchTag("KeyStoreUnavailable", (unsigned) => Effect.fail(new CallFailed({
20144
+ status: 0,
20145
+ kind: unsigned._tag,
20146
+ nextStep: unsigned.nextStep
19465
20147
  }))), Effect.timeoutOrElse({
19466
20148
  duration: timeout,
19467
20149
  orElse: () => Effect.fail(lost)
19468
- }), Effect.mapError(() => lost));
20150
+ }), Effect.mapError((error) => error instanceof CallFailed ? error : lost));
19469
20151
  const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
19470
20152
  if (response.status === 202) {
19471
20153
  const needed = decodeNeeded(body);
19472
20154
  if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
19473
20155
  yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
19474
- return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
20156
+ return yield* waitForAnswer(yield* collectAsSession(kept, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
19475
20157
  }
19476
20158
  return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
19477
20159
  });
19478
- const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
19479
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
20160
+ const collectAsSession = Effect.fnUntraced(function* (kept, approval, change) {
19480
20161
  const refused = (status) => (error) => Effect.fail(new CallFailed({
19481
20162
  status,
19482
20163
  kind: error._tag,
19483
20164
  nextStep: error.nextStep
19484
20165
  }));
19485
- return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
20166
+ return (yield* keptClient(kept).pipe(Effect.provide(signing), Effect.provideService(Ref0Config, config), Effect.provideService(HttpClient.HttpClient, http), Effect.catch(refused(0)))).approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19486
20167
  duration: requestTimeout$1,
19487
20168
  orElse: () => Effect.succeedNone
19488
20169
  }), Effect.catchTags({
19489
20170
  ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
19490
20171
  Unauthenticated: refused(401),
19491
- SessionHasEnded: refused(410)
20172
+ SessionHasEnded: refused(410),
20173
+ InvalidDpopProof: refused(401)
19492
20174
  }), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19493
20175
  });
19494
20176
  const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
@@ -19525,20 +20207,20 @@ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19525
20207
  return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
19526
20208
  });
19527
20209
  const call = Effect.fn("Ref0.call")(function* (command) {
19528
- const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
20210
+ const kept = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
19529
20211
  status: 0,
19530
20212
  kind: error._tag,
19531
20213
  nextStep: error.nextStep
19532
20214
  })));
19533
- const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
20215
+ const request = yield* (Option.isSome(kept) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
19534
20216
  status: 400,
19535
20217
  kind: refused._tag,
19536
20218
  nextStep: refused.nextStep
19537
20219
  })));
19538
20220
  const change = request.method !== "GET";
19539
- const answer = yield* Option.match(token, {
20221
+ const answer = yield* Option.match(kept, {
19540
20222
  onNone: () => asPerson(request.command, change, command.endpoint),
19541
- onSome: (bearer) => asSession(bearer, request, command.endpoint)
20223
+ onSome: (one) => asSession(one, request, command.endpoint)
19542
20224
  });
19543
20225
  if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19544
20226
  return answer;
@@ -21220,138 +21902,21 @@ const connectLineFor = Effect.fnUntraced(function* (harness, configFolder) {
21220
21902
  return `${configFolders[harness].variable}=${shellWord(configFolder)} ref0 connect`;
21221
21903
  });
21222
21904
  //#endregion
21223
- //#region src/keychain.ts
21224
- /**
21225
- * The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
21226
- * makes, and a session's token. Never a file. On macOS through `security`, on Linux through
21227
- * `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
21228
- * an argument, since any process on the machine can read another's arguments.
21229
- */
21230
- var KeychainError = class extends Schema.TaggedError()("KeychainError", {
21231
- nextStep: Schema.String,
21232
- /** The keychain answered, with an item ref0 can't read, rather than not answering. */
21233
- unreadable: Schema.optionalKey(Schema.Literal(true))
21234
- }) {};
21235
- /**
21236
- * The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
21237
- * grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
21238
- */
21239
- const keychainIn = Effect.fnUntraced(function* (service) {
21240
- const crypto = yield* Crypto.Crypto;
21241
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
21242
- const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
21243
- const unavailable = new KeychainError({ nextStep: macOS ? "The keychain didn’t answer. Unlock it, and try again." : "The keychain didn’t answer. Unlock the keychain or start a secret service, and try again." });
21244
- const missing = new KeychainError({ nextStep: "The keychain didn’t answer: ref0 needs `secret-tool`, from libsecret. Install `secret-tool` with `sudo apt install libsecret-tools` on Debian or Ubuntu, `sudo dnf install libsecret` on Fedora or `sudo pacman -S libsecret` on Arch." });
21245
- const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
21246
- const handle = yield* spawner.spawn(ChildProcess.make(command, args, { stdin: Stream.make(new TextEncoder().encode(stdin)) })).pipe(Effect.mapError((error) => !macOS && Predicate.isTagged(error.reason, "NotFound") ? missing : unavailable));
21247
- const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
21248
- const code = yield* handle.exitCode;
21249
- return {
21250
- output: output.trim(),
21251
- complaint: complaint.trim(),
21252
- code,
21253
- succeeded: code === ChildProcessSpawner.ExitCode(0)
21254
- };
21255
- })).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
21256
- const get = Effect.fn("Keychain.get")(function* (account) {
21257
- const found = macOS ? yield* run("security", [
21258
- "find-generic-password",
21259
- "-s",
21260
- service,
21261
- "-a",
21262
- account,
21263
- "-w"
21264
- ], "") : yield* run("secret-tool", [
21265
- "lookup",
21266
- "service",
21267
- service,
21268
- "account",
21269
- account
21270
- ], "");
21271
- if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
21272
- if (macOS) {
21273
- if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
21274
- return yield* unavailable;
21275
- }
21276
- if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
21277
- const listed = yield* run("secret-tool", [
21278
- "search",
21279
- "--all",
21280
- "service",
21281
- service,
21282
- "account",
21283
- account
21284
- ], "");
21285
- if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
21286
- return Option.none();
21287
- });
21288
- const set = Effect.fn("Keychain.set")(function* (account, secret) {
21289
- const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
21290
- const value = Redacted.value(secret);
21291
- if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
21292
- const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
21293
- if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
21294
- if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
21295
- "store",
21296
- "--label",
21297
- `ref0 ${account}`,
21298
- "service",
21299
- service,
21300
- "account",
21301
- account
21302
- ], Redacted.value(secret))).succeeded) return yield* unavailable;
21303
- });
21304
- const remove = Effect.fn("Keychain.remove")(function* (account) {
21305
- if (!(yield* macOS ? run("security", [
21306
- "delete-generic-password",
21307
- "-s",
21308
- service,
21309
- "-a",
21310
- account
21311
- ], "") : run("secret-tool", [
21312
- "clear",
21313
- "service",
21314
- service,
21315
- "account",
21316
- account
21317
- ], "")).succeeded) return yield* unavailable;
21318
- });
21319
- const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
21320
- return {
21321
- probe: Effect.gen(function* () {
21322
- const account = `connect-probe/${yield* random}`;
21323
- const value = Redacted.make(yield* random);
21324
- yield* Effect.gen(function* () {
21325
- yield* set(account, value);
21326
- const kept = yield* get(account);
21327
- if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
21328
- yield* remove(account);
21329
- }).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
21330
- }).pipe(Effect.timeoutOrElse({
21331
- duration: Duration.minutes(1),
21332
- orElse: () => Effect.fail(unavailable)
21333
- })),
21334
- get,
21335
- set,
21336
- remove
21337
- };
21338
- });
21339
- var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
21340
- static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
21341
- };
21342
- //#endregion
21343
21905
  //#region src/host-grant.ts
21344
21906
  /**
21345
21907
  * All lasting authority for one config folder, stored as one keychain value: the agent the folder
21346
- * acts as, by its owner, handle and agent ID, and its token. The ID is kept in the same write as
21347
- * the token, so a folder with a grant always knows which agent its commits name.
21908
+ * acts as, by its owner, handle and agent ID, its token, and the key it's bound to, if it is. The
21909
+ * ID is kept in the same write as the token, so a folder with a grant always knows which agent its
21910
+ * commits name.
21348
21911
  */
21349
21912
  const KeptHostGrant = Schema.Struct({
21350
21913
  harness: ConnectHarness,
21351
21914
  owner: OwnerName,
21352
21915
  agent: AgentHandle,
21353
21916
  agentId: AgentId,
21354
- token: Schema.RedactedFromValue(Schema.String)
21917
+ token: Schema.RedactedFromValue(Schema.String),
21918
+ /** This machine's key, by its thumbprint, when ref0 bound the grant to it: none from before. */
21919
+ cnf: Schema.optionalKey(KeyBinding)
21355
21920
  });
21356
21921
  const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
21357
21922
  const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
@@ -21360,6 +21925,15 @@ const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
21360
21925
  * read as a grant, such as one an earlier ref0 kept, still names the grant it held.
21361
21926
  */
21362
21927
  const decodeHeldToken = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({ token: KeptHostGrant.fields.token })));
21928
+ const decodeCredentialRef = Schema.decodeUnknownOption(CredentialRef);
21929
+ /**
21930
+ * A kept grant by its locator, whose it is and which row, as its token names them: what a
21931
+ * session it started, and a start it authorized, are kept beside. None for a token that isn't one.
21932
+ */
21933
+ const grantRef = (grant) => Option.flatMap(parseToken(Redacted.value(grant.token)), (parts) => decodeCredentialRef({
21934
+ person: parts.person,
21935
+ id: parts.token
21936
+ }));
21363
21937
  /** Grants in the origin’s keychain service, account host-grant/<resolved config folder>. */
21364
21938
  var HostGrant = class HostGrant extends Context.Service()("ref0/cli/HostGrant") {
21365
21939
  static layer = Layer.effect(HostGrant, Effect.gen(function* () {
@@ -21403,7 +21977,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21403
21977
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21404
21978
  * and only as a default export.
21405
21979
  */
21406
- const cliVersion = "0.12.1";
21980
+ const cliVersion = "0.14.0";
21407
21981
  //#endregion
21408
21982
  //#region src/harness/hook-protocol.ts
21409
21983
  /**
@@ -21653,6 +22227,7 @@ const yesOrNoQuestions = Effect.gen(function* () {
21653
22227
  const isAnswerCode = Schema.is(TypedApprovalCode);
21654
22228
  const decodeTokenId$1 = Schema.decodeUnknownOption(TokenId);
21655
22229
  const decodeSecretHash$1 = Schema.decodeUnknownEffect(SecretHash);
22230
+ const decodeProofOrigin = Schema.decodeUnknownEffect(ProofOrigin);
21656
22231
  /** How often the machine asks whether its person has answered. */
21657
22232
  const pollMillis = 2e3;
21658
22233
  /** How long one request may take before the machine gives up on it, and asks again. */
@@ -21705,12 +22280,14 @@ const endedWords$1 = (state) => {
21705
22280
  /** The grant a kept record holds, by its ID. */
21706
22281
  const grantOf = (token) => Option.flatMap(parseToken(Redacted.value(token)), (parts) => decodeTokenId$1(parts.token));
21707
22282
  /**
21708
- * Asks with no owner for every folder given, each with a grant secret of its own, shows the
21709
- * link, and takes the answer from the person's own page. Then it keeps each folder ref0
21710
- * connected, confirms exactly the grants it kept, and forgets 0.5.0's grants they replace.
22283
+ * Asks with no owner for every folder given, each with a grant secret of its own, signed by this
22284
+ * machine's key, which ref0 binds every grant to, shows the link, and takes the answer from the
22285
+ * person's own page. Then it keeps each folder ref0 connected, with the key it's bound to,
22286
+ * confirms exactly the grants it kept, and forgets 0.5.0's grants they replace.
21711
22287
  */
21712
- const requestAttachment = Effect.fnUntraced(function* (asked) {
22288
+ const requestAttachment = Effect.fnUntraced(function* (asked, key) {
21713
22289
  const config = yield* Ref0Config;
22290
+ const origin = yield* decodeProofOrigin(config.api).pipe(Effect.catchTag("SchemaError", () => sayFailed(`ref0 connect signs its ask only to an HTTPS origin, or HTTP on this machine, and ${config.api} is neither.`)));
21714
22291
  const machine = yield* Machine;
21715
22292
  const hostGrant = yield* HostGrant;
21716
22293
  const oldGrants = yield* OldHostGrants;
@@ -21723,7 +22300,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21723
22300
  const opener = yield* browserOpener;
21724
22301
  const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(printBlocks([["Couldn’t listen for the browser. Paste the code instead."]]), void 0)));
21725
22302
  const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
21726
- const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
22303
+ const client = yield* provingClient({
22304
+ origin,
22305
+ store: key.store
22306
+ }).pipe(Effect.provideService(HttpClient.HttpClient, http));
21727
22307
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
21728
22308
  const requestId = yield* newRequestId;
21729
22309
  const asking = yield* Effect.forEach(asked, Effect.fnUntraced(function* (folder) {
@@ -21750,6 +22330,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21750
22330
  const payload = {
21751
22331
  machine: machineId,
21752
22332
  machineName: machineNameOf(machine.hostName),
22333
+ publicKey: key.store.publicKey,
21753
22334
  folders,
21754
22335
  collectHash: yield* Effect.orDie(decodeSecretHash$1(collecting.hash)),
21755
22336
  linkHash: yield* Effect.orDie(decodeSecretHash$1(link.hash))
@@ -21765,6 +22346,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21765
22346
  NotPermitted: said,
21766
22347
  CommandRefused: said,
21767
22348
  SlowDown: said,
22349
+ KeyStoreUnavailable: said,
22350
+ InvalidDpopProof: () => sayFailed(`ref0 refused this machine’s proof of its key, so nothing was asked. Check that this machine’s clock is right, then run ${again$1} again.`),
21768
22351
  AuthorityUnavailable: unanswered,
21769
22352
  HttpClientError: unanswered,
21770
22353
  SchemaError: unanswered
@@ -21836,7 +22419,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21836
22419
  owner: connection.owner,
21837
22420
  agent: connection.handle,
21838
22421
  agentId: connection.agent.id,
21839
- token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
22422
+ token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret)),
22423
+ cnf: key.binding
21840
22424
  }, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21841
22425
  const finish = Effect.fnUntraced(function* (state) {
21842
22426
  if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
@@ -24637,14 +25221,17 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
24637
25221
  if (yield* session.current.pipe(Effect.map(Option.isSome), Effect.catchTag("NoSession", () => Effect.succeed(true)))) return yield* sayFailed("ref0 connect runs only in a person’s own terminal, never in an agent’s session: it shows a code, and a yes to that code connects this machine to an agent. If this machine needs connecting, tell your person to run ref0 connect in their own terminal. Never ask them to approve a code for you.");
24638
25222
  if (!tty.attached) return yield* sayFailed("ref0 connect runs only in a terminal you type into, and this isn’t one. Run it yourself, in your own terminal: not through a script, a pipe or an agent.");
24639
25223
  yield* (yield* ConnectLock).hold;
25224
+ const machineKey = yield* MachineKey;
25225
+ yield* machineKey.supported;
24640
25226
  yield* (yield* Keychain).probe;
25227
+ const key = yield* machineKey.ensure;
24641
25228
  const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
24642
25229
  const found = yield* findConfigFolders;
24643
25230
  if (found.length === 0) return yield* sayFailed(`ref0 found no Claude Code or Codex config folder in your home. Start Claude Code or Codex once, then run ${again} again.`);
24644
25231
  const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
24645
25232
  if (chosen.length === 0) return yield* sayFailed(`No config folder was ticked, so nothing was asked. To connect one, run ${again} again.`);
24646
25233
  yield* Console.error("");
24647
- const connected = yield* requestAttachment(chosen);
25234
+ const connected = yield* requestAttachment(chosen, key);
24648
25235
  const installed = yield* installInto(connected.kept, { asJson });
24649
25236
  const others = yield* otherOwners(connected);
24650
25237
  yield* printAnswer(answerOf(connected, installed, others), asJson, reportOf(connected, installed, others, home));
@@ -25210,9 +25797,9 @@ const signalExits = {
25210
25797
  };
25211
25798
  const decodeSetup = Schema.decodeUnknownOption(Schema.Array(Schema.String));
25212
25799
  /** A duration's milliseconds, as a check writes it, `'30 seconds'`, or a number of them. */
25213
- const millisOf = (duration) => Option.map(Duration.fromInput(duration), (read) => Math.ceil(Duration.toMillis(read)));
25800
+ const millisOf$1 = (duration) => Option.map(Duration.fromInput(duration), (read) => Math.ceil(Duration.toMillis(read)));
25214
25801
  /** A duration a command was given, in milliseconds, or why it isn't one. */
25215
- const givenMillis = (duration) => Option.match(millisOf(duration), {
25802
+ const givenMillis = (duration) => Option.match(millisOf$1(duration), {
25216
25803
  onNone: () => Effect.fail(new LocalSandboxFailed({ why: `Not a duration: ${duration}` })),
25217
25804
  onSome: Effect.succeed
25218
25805
  });
@@ -25555,7 +26142,7 @@ const defaultRetries = {
25555
26142
  };
25556
26143
  const defaultTimeout = 6e5;
25557
26144
  /** A duration as a step's config writes it, in milliseconds, or the run's code's mistake. */
25558
- const stepMillis = (duration) => Option.match(millisOf(duration), {
26145
+ const stepMillis = (duration) => Option.match(millisOf$1(duration), {
25559
26146
  onNone: () => Effect.die(/* @__PURE__ */ new Error(`Not a duration: ${duration}`)),
25560
26147
  onSome: Effect.succeed
25561
26148
  });
@@ -26700,10 +27287,9 @@ const gitTokenOf = Effect.fnUntraced(function* (binding) {
26700
27287
  const keychain = yield* Keychain;
26701
27288
  const kept = yield* keychain.get(gitTokenAccount(binding));
26702
27289
  if (Option.isSome(kept)) return kept;
26703
- const sessionToken = yield* keychain.get(sessionAccount(binding));
26704
- if (Option.isNone(sessionToken)) return Option.none();
26705
- const config = yield* Ref0Config;
26706
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(sessionToken.value)));
27290
+ const session = readKeptSession(yield* keychain.get(sessionAccount(binding)));
27291
+ if (Option.isNone(session)) return Option.none();
27292
+ const client = yield* keptClient(session.value);
26707
27293
  const me = yield* client.me.get();
26708
27294
  if (me.session === void 0) return Option.none();
26709
27295
  const { secret, hash } = yield* freshTokenSecret;
@@ -26841,12 +27427,11 @@ const trimShallow = Effect.fnUntraced(function* (gitDir) {
26841
27427
  * attempts the merge: the checkout's own for `ref0 sync`, or the local check's scratch one.
26842
27428
  */
26843
27429
  const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
26844
- const config = yield* Ref0Config;
26845
- const token = yield* (yield* SessionToken).current.pipe(Effect.catchTag("NoSession", () => Effect.succeedNone));
26846
- if (Option.isNone(token)) return yield* new HistoryNotLoaded({ why: "this terminal has no session to load it as" });
27430
+ const kept = yield* (yield* SessionToken).current.pipe(Effect.catchTag("NoSession", () => Effect.succeedNone));
27431
+ if (Option.isNone(kept)) return yield* new HistoryNotLoaded({ why: "this terminal has no session to load it as" });
26847
27432
  const fs = yield* FileSystem.FileSystem;
26848
27433
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
26849
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token.value)));
27434
+ const client = yield* keptClient(kept.value);
26850
27435
  const params = yield* decodePlace(repoParts(repo));
26851
27436
  const manifest = yield* client.repos.manifest({ params });
26852
27437
  const temp = yield* fs.makeTempDirectoryScoped();
@@ -26875,7 +27460,7 @@ const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
26875
27460
  * or the thread below, set apart.
26876
27461
  */
26877
27462
  const baseNamed = (on, main = "main") => on === "main" ? main : `#${on.thread}, which this thread is stacked on,`;
26878
- const decodeWritten$1 = Schema.decodeUnknownOption(WrittenRepo);
27463
+ const decodeWritten$2 = Schema.decodeUnknownOption(WrittenRepo);
26879
27464
  const decodeCommit$1 = Schema.decodeUnknownOption(CommitOid);
26880
27465
  const refused$1 = (why) => ({
26881
27466
  kind: "refused",
@@ -26909,7 +27494,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
26909
27494
  stdout: "inherit",
26910
27495
  stderr: "inherit"
26911
27496
  })).pipe(Effect.map(Number), Effect.catchCause(() => Effect.succeed(-1)));
26912
- const written = decodeWritten$1((yield* read([
27497
+ const written = decodeWritten$2((yield* read([
26913
27498
  "config",
26914
27499
  "--get",
26915
27500
  "ref0.repo"
@@ -30075,7 +30660,7 @@ const watchCommands = [Command.make("watch", {
30075
30660
  owner: ownerName,
30076
30661
  agent: flags.agent
30077
30662
  });
30078
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)));
30663
+ const client = yield* keptClient(grant.value);
30079
30664
  const watching = {
30080
30665
  project: flags.project,
30081
30666
  parent: Option.getOrUndefined(flags.parent),
@@ -30104,7 +30689,7 @@ const watchCommands = [Command.make("watch", {
30104
30689
  parent: number
30105
30690
  })
30106
30691
  })
30107
- }).pipe(Effect.mapError((error) => Match.value(error).pipe(Match.tag("Unauthenticated", "SessionHasEnded", () => stopped(`ref0 refused the host grant ref0 connect kept for ${as} on this machine: it was revoked or never confirmed, or the machine was disconnected. Connect it again with ref0 connect, then run ref0 watch again.`)), Match.tag("ProjectNotFound", () => stopped(`${flags.project} isn’t a project ${as} can read. Check the owner/project, and that ${flags.agent} is the agent to watch as.`)), Match.tag("ThreadNotFound", () => stopped(`${flags.project} has no thread #${Option.getOrElse(flags.parent, () => 0)} to watch the children of. Find its number with ref0 threads ${flags.project}.`)), Match.tag("NotPermitted", (refused) => stopped(refused.nextStep)), Match.tag("AuthorityUnavailable", () => new ReadUnavailable({ why: "it’s restarting" })), Match.tag("ProjectNotReady", () => new ReadUnavailable({ why: `${flags.project} isn’t ready yet` })), Match.orElse(() => new ReadUnavailable({ why: `couldn’t reach ${config.api}` })))));
30692
+ }).pipe(Effect.mapError((error) => Match.value(error).pipe(Match.tag("Unauthenticated", "SessionHasEnded", () => stopped(`ref0 refused the host grant ref0 connect kept for ${as} on this machine: it was revoked or never confirmed, or the machine was disconnected. Connect it again with ref0 connect, then run ref0 watch again.`)), Match.tag("ProjectNotFound", () => stopped(`${flags.project} isn’t a project ${as} can read. Check the owner/project, and that ${flags.agent} is the agent to watch as.`)), Match.tag("ThreadNotFound", () => stopped(`${flags.project} has no thread #${Option.getOrElse(flags.parent, () => 0)} to watch the children of. Find its number with ref0 threads ${flags.project}.`)), Match.tag("NotPermitted", "InvalidDpopProof", (refused) => stopped(refused.nextStep)), Match.tag("KeyStoreUnavailable", () => new ReadUnavailable({ why: "this machine’s key didn’t sign in time" })), Match.tag("AuthorityUnavailable", () => new ReadUnavailable({ why: "it’s restarting" })), Match.tag("ProjectNotReady", () => new ReadUnavailable({ why: `${flags.project} isn’t ready yet` })), Match.orElse(() => new ReadUnavailable({ why: `couldn’t reach ${config.api}` })))));
30108
30693
  return yield* Effect.scoped(Effect.gen(function* () {
30109
30694
  const state = yield* openWatchState(folder, {
30110
30695
  project: flags.project,
@@ -30124,7 +30709,8 @@ const watchCommands = [Command.make("watch", {
30124
30709
  }).pipe(Effect.catchTags({
30125
30710
  WatchStopped: (stopped) => sayFailed(stopped.nextStep),
30126
30711
  WatchStateError: (failed) => sayFailed(failed.nextStep),
30127
- KeychainError: (failed) => sayFailed(failed.nextStep)
30712
+ KeychainError: (failed) => sayFailed(failed.nextStep),
30713
+ InvalidDpopProof: (refused) => sayFailed(refused.nextStep)
30128
30714
  }))).pipe(Command.withShortDescription("Run a command for each thread started in a project"), Command.withDescription([
30129
30715
  "Runs a command for each thread started in a project, as one of your agents, through the host grant ref0 connect kept on this machine; it sees only what that agent may. It runs until stopped: the first time from now on, or with --parent from the parent’s first child on, and from where it stopped after that.",
30130
30716
  "Just before each run it reads the thread again, and skips it, marked done, if it’s settled or closed. Before its first run, it skips one a session is live on, with --skip-live, and one started before the watch did that a session has started on. After, a session may be one its own failed run opened, live until its lease lapses, within 30 minutes: with --skip-live, it waits until none is live, then runs again.",
@@ -30661,7 +31247,7 @@ function editedPaths(hook) {
30661
31247
  /** The most bytes of a file read around an edit; a larger one's edits aren't attributed. */
30662
31248
  const fileBytes = 2e6;
30663
31249
  /** The most edit calls waiting for their `PostToolUse`; a harness that never sends one leaks none. */
30664
- const openCalls = 64;
31250
+ const openCalls$1 = 64;
30665
31251
  /** The most line hashes a worktree keeps pending; the oldest edits go first. */
30666
31252
  const pendingLines = 2e4;
30667
31253
  /** A line as it's kept pending: 128 bits of its SHA-256, never its text. */
@@ -31030,7 +31616,7 @@ var Attribution = class Attribution extends Context.Service()("ref0/cli/Attribut
31030
31616
  turn,
31031
31617
  files
31032
31618
  });
31033
- for (const key of calls.keys()) if (calls.size > openCalls) calls.delete(key);
31619
+ for (const key of calls.keys()) if (calls.size > openCalls$1) calls.delete(key);
31034
31620
  return {
31035
31621
  ...latest,
31036
31622
  calls
@@ -31278,7 +31864,7 @@ const optionsWithValues = /* @__PURE__ */ new Set([
31278
31864
  "--git-dir",
31279
31865
  "--work-tree"
31280
31866
  ]);
31281
- const decodeWritten = Schema.decodeUnknownOption(WrittenRepo);
31867
+ const decodeWritten$1 = Schema.decodeUnknownOption(WrittenRepo);
31282
31868
  const hunkHeader = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
31283
31869
  /** The command a Git run's arguments name, past Git's own options. */
31284
31870
  const commandOf = (args) => {
@@ -31626,7 +32212,7 @@ const checkBase = Effect.fnUntraced(function* (folder, base, delivered, within)
31626
32212
  "--get",
31627
32213
  "ref0.repo"
31628
32214
  ]);
31629
- const repo = decodeWritten(named.out.trim());
32215
+ const repo = decodeWritten$1(named.out.trim());
31630
32216
  if (Option.isNone(repo)) return yield* failed("this checkout has no ref0.repo to load history for");
31631
32217
  yield* loadSyncHistory(store, repo.value).pipe(Effect.mapError((error) => failed(error.why)));
31632
32218
  yield* fs.writeFileString(loaded, "");
@@ -31948,7 +32534,7 @@ var BaseChecks = class BaseChecks extends Context.Service()("ref0/cli/BaseChecks
31948
32534
  ], { stdin: "ignore" })).pipe(Effect.map((code) => Number(code) === 0), Effect.orElseSucceed(() => false));
31949
32535
  return BaseChecks.of({
31950
32536
  settingsAt,
31951
- check: (top, base, delivered, within, token) => checkBase(top, base, delivered, within).pipe(Effect.provide(context), Effect.provideService(SessionToken, SessionToken.of({ current: Effect.succeedSome(token) }))),
32537
+ check: (top, base, delivered, within, session) => checkBase(top, base, delivered, within).pipe(Effect.provide(context), Effect.provideService(SessionToken, SessionToken.of({ current: Effect.succeedSome(session) }))),
31952
32538
  holds
31953
32539
  });
31954
32540
  }));
@@ -32001,10 +32587,10 @@ const baseWatch = Effect.fnUntraced(function* (edges) {
32001
32587
  return;
32002
32588
  }
32003
32589
  if (Option.isNone(repo)) return;
32004
- const read = yield* (yield* clientOf(bound.token)).sessions.base({ params: {
32590
+ const read = yield* clientOf(bound.kept).pipe(Effect.flatMap((client) => client.sessions.base({ params: {
32005
32591
  session: bound.session,
32006
32592
  repo: repo.value
32007
- } }).pipe(Effect.option);
32593
+ } })), Effect.option);
32008
32594
  if (Option.isNone(read)) return;
32009
32595
  const base = read.value;
32010
32596
  const mark = {
@@ -32030,7 +32616,7 @@ const baseWatch = Effect.fnUntraced(function* (edges) {
32030
32616
  commit: base.commit,
32031
32617
  url,
32032
32618
  ref: base.ref
32033
- }, delivered, clone.within, bound.token).pipe(Effect.option);
32619
+ }, delivered, clone.within, bound.kept).pipe(Effect.option);
32034
32620
  if (Option.isNone(checked)) return;
32035
32621
  const notice = checked.value.kind === "compared" ? checked.value.notice : Option.none();
32036
32622
  yield* Ref.update(watched, (was) => was.top !== clone.top ? was : {
@@ -32384,6 +32970,321 @@ var HarnessTasks = class HarnessTasks extends Context.Service()("ref0/cli/Harnes
32384
32970
  }));
32385
32971
  };
32386
32972
  //#endregion
32973
+ //#region src/harness/turn-facts.ts
32974
+ const Written = Schema.Struct({ timestamp: Schema.String });
32975
+ const decodeWritten = Schema.decodeUnknownOption(Schema.fromJsonString(Written));
32976
+ /** When a transcript's line says it was written, by the harness's clock, if it says. */
32977
+ const writtenAt = (line) => {
32978
+ const at = Option.match(decodeWritten(line), {
32979
+ onNone: () => NaN,
32980
+ onSome: (written) => Date.parse(written.timestamp)
32981
+ });
32982
+ return Number.isFinite(at) ? at : void 0;
32983
+ };
32984
+ /** The most tool names a turn's record counts. */
32985
+ const toolNames = 100;
32986
+ /** The most replies a turn's tally remembers; a turn with more has its tokens unknown. */
32987
+ const turnReplies = 1e4;
32988
+ /**
32989
+ * The most calls a turn remembers as begun and not ended, its newest kept: a call that never
32990
+ * ends, as one a `PreToolUse` answer denied, is never counted.
32991
+ */
32992
+ const openCalls = 100;
32993
+ const nothingRead = {
32994
+ known: "waiting",
32995
+ models: /* @__PURE__ */ new Map(),
32996
+ replies: /* @__PURE__ */ new Map()
32997
+ };
32998
+ const nothingReadOf = {
32999
+ tokens: nothingRead,
33000
+ context: {
33001
+ known: "waiting",
33002
+ peak: void 0,
33003
+ compactions: []
33004
+ },
33005
+ subagents: /* @__PURE__ */ new Map(),
33006
+ moreSubagents: 0
33007
+ };
33008
+ const nothingCalled = {
33009
+ known: true,
33010
+ totalMs: 0,
33011
+ longest: void 0,
33012
+ open: /* @__PURE__ */ new Map()
33013
+ };
33014
+ const nothingSeen = {
33015
+ ...nothingReadOf,
33016
+ tools: /* @__PURE__ */ new Map(),
33017
+ calls: nothingCalled,
33018
+ addresses: [],
33019
+ more: 0,
33020
+ model: void 0,
33021
+ startedAt: void 0,
33022
+ stoppedAt: void 0
33023
+ };
33024
+ /**
33025
+ * A call's `PreToolUse` or its end seen at `at`: its time Claude Code's `duration_ms`, or, where
33026
+ * the harness says none, as Codex, from its `PreToolUse` to its end. One whose time can't be
33027
+ * told makes the turn's unknown, as does a tool name ref0 refuses.
33028
+ */
33029
+ function timeCall(calls, hook, at) {
33030
+ const name = hook.tool?.name;
33031
+ const callId = hook.tool?.callId;
33032
+ if (name === void 0) return calls;
33033
+ if (hook.event === "PreToolUse") {
33034
+ if (callId === void 0) return calls;
33035
+ const open = new Map(calls.open).set(callId, at);
33036
+ for (const [oldest] of open) {
33037
+ if (open.size <= openCalls) break;
33038
+ open.delete(oldest);
33039
+ }
33040
+ return {
33041
+ ...calls,
33042
+ open
33043
+ };
33044
+ }
33045
+ if (hook.event !== "PostToolUse" && hook.event !== "PostToolUseFailure") return calls;
33046
+ const open = new Map(calls.open);
33047
+ const began = callId === void 0 ? void 0 : open.get(callId);
33048
+ if (callId !== void 0) open.delete(callId);
33049
+ const ms = hook.durationMs ?? (began === void 0 ? void 0 : Math.max(at - began, 0));
33050
+ if (ms === void 0 || name.length > 128) return {
33051
+ ...calls,
33052
+ known: false,
33053
+ open
33054
+ };
33055
+ const longest = calls.longest === void 0 || ms > calls.longest.ms ? {
33056
+ tool: name,
33057
+ ms
33058
+ } : calls.longest;
33059
+ return {
33060
+ ...calls,
33061
+ totalMs: calls.totalMs + ms,
33062
+ longest,
33063
+ open
33064
+ };
33065
+ }
33066
+ /**
33067
+ * A hook seen at `at`: a tool call counted by its tool once it ran, and timed, the files an edit
33068
+ * is about to touch kept, at most 50 and the rest counted, the model, where the hook names one,
33069
+ * and a `Stop` as when the agent last stopped.
33070
+ */
33071
+ function seeHook(seen, hook, edited, at) {
33072
+ const tools = new Map(seen.tools);
33073
+ const name = hook.tool?.name;
33074
+ if (hook.event === "PostToolUse" && name !== void 0) {
33075
+ const counted = tools.get(name);
33076
+ if (counted !== void 0 || tools.size < toolNames) tools.set(name, (counted ?? 0) + 1);
33077
+ }
33078
+ const addresses = [...seen.addresses];
33079
+ let more = seen.more;
33080
+ for (const each of edited) {
33081
+ if (addresses.some((kept) => kept.repo === each.repo && kept.path === each.path)) continue;
33082
+ if (addresses.length < 50) addresses.push(each);
33083
+ else more += 1;
33084
+ }
33085
+ return {
33086
+ ...seen,
33087
+ tools,
33088
+ calls: timeCall(seen.calls, hook, at),
33089
+ addresses,
33090
+ more,
33091
+ model: hook.model ?? seen.model,
33092
+ stoppedAt: hook.event === "Stop" ? at : seen.stoppedAt
33093
+ };
33094
+ }
33095
+ /**
33096
+ * A read's replies added to a turn's, each by its message ID at the most any of its lines says:
33097
+ * a subagent's transcript writes a reply's output as it grows, line by line, so a line of a reply
33098
+ * already counted adds only what it grew. `own` when the read was of the session's own
33099
+ * transcript, which makes the turn's tokens known; nothing makes them known once a read has
33100
+ * failed.
33101
+ */
33102
+ function addReplies(tally, replies, own) {
33103
+ if (tally.known === "failed") return tally;
33104
+ const models = new Map(tally.models);
33105
+ const counted = new Map(tally.replies);
33106
+ for (const reply of replies) {
33107
+ const before = counted.get(reply.id);
33108
+ const most = {
33109
+ model: before?.model ?? reply.model,
33110
+ input: Math.max(before?.input ?? 0, reply.input),
33111
+ output: Math.max(before?.output ?? 0, reply.output),
33112
+ cacheWrite: Math.max(before?.cacheWrite ?? 0, reply.cacheWrite),
33113
+ cacheRead: Math.max(before?.cacheRead ?? 0, reply.cacheRead)
33114
+ };
33115
+ const sum = models.get(most.model);
33116
+ counted.set(reply.id, most);
33117
+ models.set(most.model, {
33118
+ input: (sum?.input ?? 0) + most.input - (before?.input ?? 0),
33119
+ output: (sum?.output ?? 0) + most.output - (before?.output ?? 0),
33120
+ cacheWrite: (sum?.cacheWrite ?? 0) + most.cacheWrite - (before?.cacheWrite ?? 0),
33121
+ cacheRead: (sum?.cacheRead ?? 0) + most.cacheRead - (before?.cacheRead ?? 0)
33122
+ });
33123
+ }
33124
+ if (counted.size > turnReplies) return readFailed(tally);
33125
+ return {
33126
+ known: own ? "read" : tally.known,
33127
+ models,
33128
+ replies: counted
33129
+ };
33130
+ }
33131
+ /**
33132
+ * A read's requests added to a turn's, by model, as Codex's rollout steps its running total.
33133
+ * `own` as for `addReplies`: a rollout is always the session's own.
33134
+ */
33135
+ function addSteps(tally, steps, own) {
33136
+ if (tally.known === "failed") return tally;
33137
+ const models = new Map(tally.models);
33138
+ for (const step of steps) {
33139
+ const sum = models.get(step.model);
33140
+ models.set(step.model, {
33141
+ input: (sum?.input ?? 0) + step.input,
33142
+ output: (sum?.output ?? 0) + step.output,
33143
+ cacheWrite: (sum?.cacheWrite ?? 0) + step.cacheWrite,
33144
+ cacheRead: (sum?.cacheRead ?? 0) + step.cacheRead
33145
+ });
33146
+ }
33147
+ return {
33148
+ ...tally,
33149
+ known: own ? "read" : tally.known,
33150
+ models
33151
+ };
33152
+ }
33153
+ /** A read that failed: the turn's tokens are unknown from then on. */
33154
+ const readFailed = (tally) => ({
33155
+ ...tally,
33156
+ known: "failed"
33157
+ });
33158
+ /** A read's replies and requests added to a turn's tokens, or its tokens unknown if it failed. */
33159
+ const addTokens = (tally, read, own) => read.whole ? addSteps(addReplies(tally, read.replies, own), read.steps, own) : readFailed(tally);
33160
+ /** What one request sent: its input, cached or not. */
33161
+ const sentBy = (tokens) => tokens.input + tokens.cacheWrite + tokens.cacheRead;
33162
+ /**
33163
+ * A read of the session's own transcript added to its context: the most any request sent, each
33164
+ * Claude Code reply at the most its lines said, and each compaction. Unknown once a read fails.
33165
+ */
33166
+ function addContext(context, read) {
33167
+ if (context.known === "failed") return context;
33168
+ if (!read.whole) return {
33169
+ ...context,
33170
+ known: "failed"
33171
+ };
33172
+ return {
33173
+ known: "read",
33174
+ peak: [...read.replies, ...read.steps].reduce((most, request) => Math.max(most ?? 0, sentBy(request)), context.peak),
33175
+ compactions: [...context.compactions, ...read.compactions]
33176
+ };
33177
+ }
33178
+ /**
33179
+ * A read added to a turn's: to its tokens, and, from the session's own transcript, to its
33180
+ * context, or, from a subagent's, to that subagent's own tokens too, each subagent kept by its
33181
+ * transcript as first read, past the first 100 only counted.
33182
+ */
33183
+ function addRead(reads, read, of) {
33184
+ const tokens = addTokens(reads.tokens, read, of.own);
33185
+ if (of.own) return {
33186
+ ...reads,
33187
+ tokens,
33188
+ context: addContext(reads.context, read)
33189
+ };
33190
+ const before = reads.subagents.get(of.subagent);
33191
+ if (before === void 0 && reads.subagents.size >= 100) return {
33192
+ ...reads,
33193
+ tokens,
33194
+ moreSubagents: reads.moreSubagents + 1
33195
+ };
33196
+ const subagent = {
33197
+ type: before?.type ?? of.type,
33198
+ tokens: addTokens(before?.tokens ?? nothingRead, read, true)
33199
+ };
33200
+ return {
33201
+ ...reads,
33202
+ tokens,
33203
+ subagents: new Map(reads.subagents).set(of.subagent, subagent)
33204
+ };
33205
+ }
33206
+ /**
33207
+ * A turn's tokens as ref0 keeps them: each model's counts once the session's own transcript is
33208
+ * read, or unknown, with no counts, where it wasn't, a read failed, or a count passes what a
33209
+ * turn may hold.
33210
+ */
33211
+ function turnTokens(tally) {
33212
+ if (tally.known !== "read" || tally.models.size > 16) return { known: "unknown" };
33213
+ const models = [...tally.models].map(([model, counts]) => ({
33214
+ model,
33215
+ ...counts
33216
+ }));
33217
+ return models.every((each) => Math.max(each.input, each.output, each.cacheWrite, each.cacheRead) <= 1e8 && each.model.length >= 1 && each.model.length <= 128) ? {
33218
+ known: "read",
33219
+ models
33220
+ } : { known: "unknown" };
33221
+ }
33222
+ /** A turn's tool calls' time as ref0 keeps it: unknown once a call's time couldn't be told. */
33223
+ function toolTime(calls) {
33224
+ if (!calls.known) return { known: "unknown" };
33225
+ return calls.longest === void 0 ? {
33226
+ known: "timed",
33227
+ totalMs: calls.totalMs
33228
+ } : {
33229
+ known: "timed",
33230
+ totalMs: calls.totalMs,
33231
+ longest: calls.longest
33232
+ };
33233
+ }
33234
+ /**
33235
+ * A turn's context as ref0 keeps it, each compaction's time as how long after ref0 started the
33236
+ * turn, 0 for one before its start landed: unknown until the session's own transcript is read,
33237
+ * once a read fails, or with more compactions than a turn names.
33238
+ */
33239
+ function turnContext(context, startedAt) {
33240
+ if (context.known !== "read" || context.compactions.length > 100) return { known: "unknown" };
33241
+ if (context.peak !== void 0 && context.peak > 1e8) return { known: "unknown" };
33242
+ const compactions = context.compactions.map((each) => {
33243
+ const compaction = {};
33244
+ if (each.trigger !== void 0) compaction.trigger = each.trigger;
33245
+ if (each.at !== void 0 && startedAt !== void 0) compaction.afterMs = Math.max(each.at - startedAt, 0);
33246
+ return compaction;
33247
+ });
33248
+ return context.peak === void 0 ? {
33249
+ known: "read",
33250
+ compactions
33251
+ } : {
33252
+ known: "read",
33253
+ peak: context.peak,
33254
+ compactions
33255
+ };
33256
+ }
33257
+ /**
33258
+ * The facts a turn's reads give ref0: its tokens, its context and each subagent's tokens, with
33259
+ * its type where the harness named it.
33260
+ */
33261
+ const readFacts = (reads, startedAt) => ({
33262
+ tokens: turnTokens(reads.tokens),
33263
+ context: turnContext(reads.context, startedAt),
33264
+ subagents: [...reads.subagents.values()].map((subagent) => subagent.type === void 0 || subagent.type.length > 128 ? { tokens: turnTokens(subagent.tokens) } : {
33265
+ type: subagent.type,
33266
+ tokens: turnTokens(subagent.tokens)
33267
+ }),
33268
+ moreSubagents: reads.moreSubagents
33269
+ });
33270
+ /**
33271
+ * A turn's facts as ref0 keeps them, with its stop as how long after ref0 started it the agent last
33272
+ * stopped: none until both are seen, and 0 for a `Stop` before its start landed.
33273
+ */
33274
+ function turnFacts(seen) {
33275
+ const facts = {
33276
+ modelSource: seen.model === void 0 ? "unknown" : "hook",
33277
+ tools: Object.fromEntries(seen.tools),
33278
+ addresses: seen.addresses,
33279
+ more: seen.more,
33280
+ toolTime: toolTime(seen.calls),
33281
+ ...readFacts(seen, seen.startedAt)
33282
+ };
33283
+ if (seen.model !== void 0) facts.model = seen.model;
33284
+ if (seen.startedAt !== void 0 && seen.stoppedAt !== void 0) facts.stoppedAfterMs = Math.max(seen.stoppedAt - seen.startedAt, 0);
33285
+ return facts;
33286
+ }
33287
+ //#endregion
32387
33288
  //#region src/harness/rollouts.ts
32388
33289
  /**
32389
33290
  * Codex's rollouts, as `ref0 mcp` reads a turn's tokens from them: each hook names the
@@ -32403,7 +33304,8 @@ var HarnessTasks = class HarnessTasks extends Context.Service()("ref0/cli/Harnes
32403
33304
  * `token_count`s, then the subagent's own turn, its total from zero, a forked subagent's as a
32404
33305
  * fresh one's (Codex 0.163.0's subagents, 20 Oct 2026). So its requests are those past its own
32405
33306
  * turn's `task_started`, and what its total carried over, nothing so far, is its first request's
32406
- * total less that request's own usage.
33307
+ * total less that request's own usage. Each compaction of the session's context writes a
33308
+ * `compacted` record, with its time and none of what started it (Codex 0.163.0's, 23 Oct 2026).
32407
33309
  */
32408
33310
  const Count$1 = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
32409
33311
  /** A session's running total, as far as ref0 counts it: reasoning is inside output. */
@@ -32451,12 +33353,16 @@ const zero = {
32451
33353
  cacheWrite: 0,
32452
33354
  output: 0
32453
33355
  };
32454
- /** The records a read decodes: each request's total, each turn's model, start and cutting short. */
33356
+ /**
33357
+ * The records a read decodes: each request's total, each turn's model, start and cutting short,
33358
+ * and each compaction.
33359
+ */
32455
33360
  const counted = [
32456
33361
  "\"token_count\"",
32457
33362
  "\"turn_context\"",
32458
33363
  "\"task_started\"",
32459
- "\"turn_aborted\""
33364
+ "\"turn_aborted\"",
33365
+ "\"compacted\""
32460
33366
  ];
32461
33367
  /** A line, as far as telling Codex's records apart, and the turn an event is of. */
32462
33368
  const Line$1 = Schema.Struct({
@@ -32541,6 +33447,7 @@ const tokensOf = (model, step) => ({
32541
33447
  */
32542
33448
  function stepsIn(text, from, until) {
32543
33449
  const steps = [];
33450
+ const compactions = [];
32544
33451
  const lines = text.split("\n");
32545
33452
  let { top, total, model } = from;
32546
33453
  let whole = true;
@@ -32557,6 +33464,7 @@ function stepsIn(text, from, until) {
32557
33464
  const event = kind.value.type === "event_msg" ? kind.value.payload : void 0;
32558
33465
  if (event?.type === "task_started" && until !== void 0 && event.turn_id === until) return {
32559
33466
  steps,
33467
+ compactions,
32560
33468
  running: {
32561
33469
  top,
32562
33470
  total,
@@ -32565,6 +33473,11 @@ function stepsIn(text, from, until) {
32565
33473
  whole,
32566
33474
  before: index
32567
33475
  };
33476
+ if (kind.value.type === "compacted") {
33477
+ const at = writtenAt(line);
33478
+ compactions.push(at === void 0 ? {} : { at });
33479
+ continue;
33480
+ }
32568
33481
  if (event?.type === "turn_aborted") {
32569
33482
  whole = false;
32570
33483
  continue;
@@ -32595,6 +33508,7 @@ function stepsIn(text, from, until) {
32595
33508
  }
32596
33509
  return {
32597
33510
  steps,
33511
+ compactions,
32598
33512
  running: {
32599
33513
  top,
32600
33514
  total,
@@ -32612,7 +33526,9 @@ function stepsIn(text, from, until) {
32612
33526
  * Each holds one JSON object a line, and writes an assistant reply over several lines, each with
32613
33527
  * the reply's `message.usage` so far: the session's own repeat it, and a subagent's grow its
32614
33528
  * output line by line. So a reply counts once, by its `message.id`, at the most its lines say,
32615
- * with its own model (Claude Code 2.1.304's transcripts, 20 Oct 2026). Codex's: the session's
33529
+ * with its own model (Claude Code 2.1.304's transcripts, 20 Oct 2026), and a compaction as a
33530
+ * `system` line, `compact_boundary`, with its time and its trigger (2.1.310's, 24 Oct 2026).
33531
+ * Codex's: the session's
32616
33532
  * rollout, whose running total `rollouts.ts` reads, kept with its place, and each subagent's,
32617
33533
  * which its `SubagentStop` names, read first from where its own turn starts, past the parent's
32618
33534
  * history it copies. Each is read from the
@@ -32638,8 +33554,13 @@ const linesEnd = (bytes, count) => {
32638
33554
  return end;
32639
33555
  };
32640
33556
  const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
32641
- /** A line, as far as telling an assistant's from the rest. */
32642
- const Line = Schema.Struct({ type: Schema.String });
33557
+ /** A line, as far as telling an assistant's and a compaction's from the rest. */
33558
+ const Line = Schema.Struct({
33559
+ type: Schema.String,
33560
+ subtype: Schema.optionalKey(Schema.Unknown)
33561
+ });
33562
+ /** A compaction's line, as far as what started it. */
33563
+ const CompactionLine = Schema.Struct({ compactMetadata: Schema.Struct({ trigger: Schema.Literals(["auto", "manual"]) }) });
32643
33564
  /** An assistant line: a reply's ID, its model and its usage, which every line of it repeats. */
32644
33565
  const AssistantLine = Schema.Struct({ message: Schema.Struct({
32645
33566
  model: Schema.String,
@@ -32653,18 +33574,34 @@ const AssistantLine = Schema.Struct({ message: Schema.Struct({
32653
33574
  }) });
32654
33575
  const decodeLine = Schema.decodeUnknownOption(Schema.fromJsonString(Line));
32655
33576
  const decodeAssistant = Schema.decodeUnknownOption(Schema.fromJsonString(AssistantLine));
33577
+ const decodeCompaction = Schema.decodeUnknownOption(Schema.fromJsonString(CompactionLine));
33578
+ /** A compaction's line read: one is counted whatever else it says. */
33579
+ const compactionOf = (line) => {
33580
+ const trigger = Option.getOrUndefined(decodeCompaction(line))?.compactMetadata.trigger;
33581
+ const at = writtenAt(line);
33582
+ const compaction = {};
33583
+ if (trigger !== void 0) compaction.trigger = trigger;
33584
+ if (at !== void 0) compaction.at = at;
33585
+ return compaction;
33586
+ };
32656
33587
  /** What Claude Code names the model of a reply it wrote itself, such as an error: no model's. */
32657
33588
  const synthetic = "<synthetic>";
32658
33589
  /**
32659
- * The replies in a transcript's whole lines, in order, or none if an assistant line can't be read,
32660
- * since its tokens would go missing unseen. A line that isn't JSON is passed over: a read that
32661
- * starts inside a line, after a transcript shrank, reads its tail as one.
33590
+ * The replies and compactions in a transcript's whole lines, in order, or none if an assistant
33591
+ * line can't be read, since its tokens would go missing unseen. A line that isn't JSON is passed
33592
+ * over: a read that starts inside a line, after a transcript shrank, reads its tail as one.
32662
33593
  */
32663
- function repliesIn(text) {
33594
+ function transcriptIn(text) {
32664
33595
  const replies = [];
33596
+ const compactions = [];
32665
33597
  for (const line of text.split("\n")) {
32666
33598
  const kind = decodeLine(line);
32667
- if (Option.isNone(kind) || kind.value.type !== "assistant") continue;
33599
+ if (Option.isNone(kind)) continue;
33600
+ if (kind.value.type === "system" && kind.value.subtype === "compact_boundary") {
33601
+ compactions.push(compactionOf(line));
33602
+ continue;
33603
+ }
33604
+ if (kind.value.type !== "assistant") continue;
32668
33605
  const assistant = decodeAssistant(line);
32669
33606
  if (Option.isNone(assistant)) return Option.none();
32670
33607
  const { model, id, usage } = assistant.value.message;
@@ -32679,8 +33616,17 @@ function repliesIn(text) {
32679
33616
  cacheRead: usage.cache_read_input_tokens ?? 0
32680
33617
  });
32681
33618
  }
32682
- return Option.some(replies);
33619
+ return Option.some({
33620
+ replies,
33621
+ compactions
33622
+ });
32683
33623
  }
33624
+ /** A read that found nothing. */
33625
+ const nothingIn = {
33626
+ replies: [],
33627
+ steps: [],
33628
+ compactions: []
33629
+ };
32684
33630
  /** A transcript never read: its top. */
32685
33631
  const top = {
32686
33632
  offset: 0,
@@ -32713,6 +33659,23 @@ const KeptCounts = Schema.Struct({
32713
33659
  cacheWrite: Count,
32714
33660
  cacheRead: Count
32715
33661
  });
33662
+ const Known = Schema.Literals([
33663
+ "waiting",
33664
+ "read",
33665
+ "failed"
33666
+ ]);
33667
+ const KeptTally = Schema.Struct({
33668
+ known: Known,
33669
+ models: Schema.Array(Schema.Struct({
33670
+ model: Schema.String,
33671
+ ...KeptCounts.fields
33672
+ })),
33673
+ replies: Schema.Array(Schema.Struct({
33674
+ id: Schema.String,
33675
+ model: Schema.String,
33676
+ ...KeptCounts.fields
33677
+ }))
33678
+ });
32716
33679
  const KeptFile = Schema.Struct({
32717
33680
  generation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
32718
33681
  read: Schema.Array(Schema.Struct({
@@ -32725,24 +33688,42 @@ const KeptFile = Schema.Struct({
32725
33688
  turn: Schema.optionalKey(Schema.Struct({
32726
33689
  number: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
32727
33690
  facts: TurnFacts,
32728
- known: Schema.Literals([
32729
- "waiting",
32730
- "read",
32731
- "failed"
32732
- ]),
32733
- models: Schema.Array(Schema.Struct({
32734
- model: Schema.String,
32735
- ...KeptCounts.fields
33691
+ startedAt: Schema.optionalKey(Count),
33692
+ tokens: KeptTally,
33693
+ context: Schema.Struct({
33694
+ known: Known,
33695
+ peak: Schema.optionalKey(Count),
33696
+ compactions: Schema.Array(Schema.Struct({
33697
+ trigger: Schema.optionalKey(Schema.Literals(["auto", "manual"])),
33698
+ at: Schema.optionalKey(Count)
33699
+ }))
33700
+ }),
33701
+ subagents: Schema.Array(Schema.Struct({
33702
+ transcript: Schema.String,
33703
+ type: Schema.optionalKey(Schema.String),
33704
+ tokens: KeptTally
32736
33705
  })),
32737
- replies: Schema.Array(Schema.Struct({
32738
- id: Schema.String,
32739
- model: Schema.String,
32740
- ...KeptCounts.fields
32741
- }))
33706
+ moreSubagents: Count
32742
33707
  }))
32743
33708
  });
32744
33709
  const encodeKept = Schema.encodeSync(Schema.fromJsonString(KeptFile));
32745
33710
  const decodeKept = Schema.decodeUnknownOption(Schema.fromJsonString(KeptFile));
33711
+ const tallyFromFile = (tally) => ({
33712
+ known: tally.known,
33713
+ models: new Map(tally.models.map(({ model, ...counts }) => [model, counts])),
33714
+ replies: new Map(tally.replies.map(({ id, ...reply }) => [id, reply]))
33715
+ });
33716
+ const tallyToFile = (tally) => ({
33717
+ known: tally.known,
33718
+ models: [...tally.models].map(([model, counts]) => ({
33719
+ model,
33720
+ ...counts
33721
+ })),
33722
+ replies: [...tally.replies].map(([id, reply]) => ({
33723
+ id,
33724
+ ...reply
33725
+ }))
33726
+ });
32746
33727
  const fromFile = (file) => ({
32747
33728
  generation: file.generation,
32748
33729
  places: new Map(file.read.map(({ path, ...place }) => [path, place])),
@@ -32750,10 +33731,19 @@ const fromFile = (file) => ({
32750
33731
  turn: Option.fromNullishOr(file.turn).pipe(Option.map((turn) => ({
32751
33732
  number: turn.number,
32752
33733
  facts: turn.facts,
32753
- tokens: {
32754
- known: turn.known,
32755
- models: new Map(turn.models.map(({ model, ...counts }) => [model, counts])),
32756
- replies: new Map(turn.replies.map(({ id, ...reply }) => [id, reply]))
33734
+ startedAt: turn.startedAt,
33735
+ reads: {
33736
+ tokens: tallyFromFile(turn.tokens),
33737
+ context: {
33738
+ known: turn.context.known,
33739
+ peak: turn.context.peak,
33740
+ compactions: turn.context.compactions
33741
+ },
33742
+ subagents: new Map(turn.subagents.map(({ transcript, type, tokens }) => [transcript, {
33743
+ type,
33744
+ tokens: tallyFromFile(tokens)
33745
+ }])),
33746
+ moreSubagents: turn.moreSubagents
32757
33747
  }
32758
33748
  })))
32759
33749
  });
@@ -32767,20 +33757,32 @@ const toFile = (kept) => {
32767
33757
  own: kept.own
32768
33758
  };
32769
33759
  if (Option.isSome(kept.turn)) {
32770
- const { number, facts, tokens } = kept.turn.value;
32771
- file.turn = {
33760
+ const { number, facts, reads, startedAt } = kept.turn.value;
33761
+ const { known, peak, compactions } = reads.context;
33762
+ const turn = {
32772
33763
  number,
32773
33764
  facts,
32774
- known: tokens.known,
32775
- models: [...tokens.models].map(([model, counts]) => ({
32776
- model,
32777
- ...counts
32778
- })),
32779
- replies: [...tokens.replies].map(([id, reply]) => ({
32780
- id,
32781
- ...reply
32782
- }))
33765
+ tokens: tallyToFile(reads.tokens),
33766
+ context: peak === void 0 ? {
33767
+ known,
33768
+ compactions
33769
+ } : {
33770
+ known,
33771
+ peak,
33772
+ compactions
33773
+ },
33774
+ subagents: [...reads.subagents].map(([transcript, { type, tokens }]) => type === void 0 ? {
33775
+ transcript,
33776
+ tokens: tallyToFile(tokens)
33777
+ } : {
33778
+ transcript,
33779
+ type,
33780
+ tokens: tallyToFile(tokens)
33781
+ }),
33782
+ moreSubagents: reads.moreSubagents
32783
33783
  };
33784
+ if (startedAt !== void 0) turn.startedAt = startedAt;
33785
+ file.turn = turn;
32784
33786
  }
32785
33787
  return file;
32786
33788
  };
@@ -32877,8 +33879,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32877
33879
  const readOne = Effect.fnUntraced(function* (harness, transcript, from, until) {
32878
33880
  const info = yield* fs.stat(transcript);
32879
33881
  if (info.type !== "File") return {
32880
- replies: [],
32881
- steps: [],
33882
+ ...nothingIn,
32882
33883
  whole: false,
32883
33884
  to: from,
32884
33885
  reached: false
@@ -32890,6 +33891,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32890
33891
  const lost = harness === "codex" && (!same || from.running === void 0);
32891
33892
  const replies = [];
32892
33893
  const steps = [];
33894
+ const compactions = [];
32893
33895
  let at = lost ? yield* tailOf(file, size) : same ? from.offset : 0;
32894
33896
  let running = lost ? totalLost : from.running;
32895
33897
  let whole = !lost;
@@ -32908,6 +33910,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32908
33910
  if (harness === "codex") {
32909
33911
  const found = stepsIn(text, running ?? totalLost, until);
32910
33912
  steps.push(...found.steps);
33913
+ compactions.push(...found.compactions);
32911
33914
  running = found.running;
32912
33915
  whole &&= found.whole;
32913
33916
  if (found.before !== void 0) {
@@ -32916,8 +33919,11 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32916
33919
  break;
32917
33920
  }
32918
33921
  } else {
32919
- const found = repliesIn(text);
32920
- replies.push(...Option.getOrElse(found, () => []));
33922
+ const found = transcriptIn(text);
33923
+ if (Option.isSome(found)) {
33924
+ replies.push(...found.value.replies);
33925
+ compactions.push(...found.value.compactions);
33926
+ }
32921
33927
  whole &&= Option.isSome(found);
32922
33928
  }
32923
33929
  at += lines;
@@ -32926,6 +33932,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32926
33932
  return {
32927
33933
  replies,
32928
33934
  steps,
33935
+ compactions,
32929
33936
  whole,
32930
33937
  to: placeOf(at, hash, running),
32931
33938
  reached
@@ -32978,25 +33985,27 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32978
33985
  const state = yield* stateOf(owner);
32979
33986
  const replies = [];
32980
33987
  const steps = [];
33988
+ const compactions = [];
32981
33989
  const places = /* @__PURE__ */ new Map();
32982
33990
  let whole = true;
32983
33991
  for (const transcript of transcripts) {
32984
33992
  const since = harness === "codex" ? turn?.since : void 0;
32985
33993
  const from = state.places.get(transcript) ?? (since === void 0 ? top : yield* subagentStartOf(transcript, since));
32986
33994
  const one = yield* readOne(harness, transcript, from, turn?.until).pipe(Effect.orElseSucceed(() => ({
32987
- replies: [],
32988
- steps: [],
33995
+ ...nothingIn,
32989
33996
  whole: false,
32990
33997
  to: from
32991
33998
  })));
32992
33999
  replies.push(...one.replies);
32993
34000
  steps.push(...one.steps);
34001
+ compactions.push(...one.compactions);
32994
34002
  places.set(transcript, one.to);
32995
34003
  whole &&= one.whole;
32996
34004
  }
32997
34005
  return {
32998
34006
  replies,
32999
34007
  steps,
34008
+ compactions,
33000
34009
  places,
33001
34010
  whole
33002
34011
  };
@@ -33181,6 +34190,8 @@ function withContext(answer, event, delivered) {
33181
34190
  //#region src/harness/hook-input.ts
33182
34191
  const decodeObjectText = Schema.decodeUnknownOption(Schema.fromJsonString(JsonObject));
33183
34192
  const filled = (text) => text === "" || text === null ? void 0 : text;
34193
+ /** Milliseconds as a hook's text says them, rounded; none for text that isn't a number. */
34194
+ const millisOf = (text) => text !== void 0 && /^\d{1,15}(\.\d+)?$/.test(text) ? Math.round(Number(text)) : void 0;
33184
34195
  const toolValue = (value) => {
33185
34196
  if (value === void 0 || value === "") return void 0;
33186
34197
  if (value instanceof Object) return value;
@@ -33194,22 +34205,26 @@ function readHook(args) {
33194
34205
  };
33195
34206
  const turn = filled(args.prompt_id) ?? filled(args.turn_id);
33196
34207
  const agent = filled(args.agent_id);
34208
+ const agentType = filled(args.agent_type);
33197
34209
  const name = filled(args.tool_name);
33198
34210
  const callId = filled(args.tool_use_id);
33199
34211
  const input = toolValue(args.tool_input);
33200
34212
  const response = toolValue(args.tool_response);
34213
+ const durationMs = millisOf(filled(args.duration_ms));
33201
34214
  const model = filled(args.model);
33202
34215
  const cwd = filled(args.cwd);
33203
34216
  const transcript = filled(args.transcript_path);
33204
34217
  const agentTranscript = filled(args.agent_transcript_path);
33205
34218
  if (turn !== void 0) hook.harnessTurn = turn;
33206
34219
  if (agent !== void 0) hook.agent = agent;
34220
+ if (agentType !== void 0) hook.agentType = agentType;
33207
34221
  if (name !== void 0) hook.tool = callId === void 0 ? { name } : {
33208
34222
  name,
33209
34223
  callId
33210
34224
  };
33211
34225
  if (input instanceof Object) hook.toolInput = input;
33212
34226
  if (response !== void 0) hook.toolResponse = response;
34227
+ if (durationMs !== void 0) hook.durationMs = durationMs;
33213
34228
  if (model !== void 0) hook.model = model;
33214
34229
  if (cwd !== void 0) hook.cwd = cwd;
33215
34230
  if (transcript !== void 0) hook.transcript = transcript;
@@ -33224,28 +34239,31 @@ function readHook(args) {
33224
34239
  * `ref0 clone` writes), or, with none, on a new thread under a plain title, such as "Claude Code
33225
34240
  * in shop, 9 Oct 14:02", which the agent then names in its own words (decided, 9 Oct 2026, 3).
33226
34241
  * The prompt's words never become the title. A session `ref0 watch` started says so, with the
33227
- * event that started its thread, as its trigger. The session's token is kept in the keychain
33228
- * under its binding's account, and nowhere else, and its thread beside it, so the session that
33229
- * follows one that ended goes on the same thread.
34242
+ * event that started its thread, as its trigger. The session's credential is kept in the
34243
+ * keychain under its binding's account, and nowhere else, as one record with the key it's bound
34244
+ * to and the grant that started it, and its thread beside it, so the session that follows one
34245
+ * that ended goes on the same thread.
33230
34246
  */
33231
34247
  /** The keychain account a binding's thread is kept under, beside its session token's. */
33232
34248
  const threadAccount = (binding) => `${sessionAccount(binding)}/thread`;
33233
34249
  /** The keychain account a start not yet landed is kept under, beside its session token's. */
33234
34250
  const pendingAccount = (binding) => `${sessionAccount(binding)}/pending`;
33235
34251
  const decodeRequest = Schema.decodeUnknownEffect(StartSessionRequest);
33236
- const decodeRequestId = Schema.decodeUnknownOption(RequestId);
33237
34252
  /**
33238
- * A pending start's request ID and secret, as kept: `<request ID> <secret>`. One whose secret
33239
- * isn't a token's, kept before tokens took only those, could never authorize, so it's dropped.
34253
+ * A start not yet landed, as kept: its request ID and secret, and the grant that authorized it
34254
+ * and the key it was signed by, so it's sent again only by that grant, with that key. One kept
34255
+ * in any other form is dropped, and the next start is a new one.
33240
34256
  */
33241
- const decodePending = (kept) => {
33242
- const [requestId = "", secret = ""] = kept.split(" ");
33243
- if (!isTokenSecret(secret)) return Option.none();
33244
- return Option.map(decodeRequestId(requestId), (id) => ({
33245
- requestId: id,
33246
- secret
33247
- }));
33248
- };
34257
+ const PendingStart = Schema.Struct({
34258
+ requestId: RequestId,
34259
+ secret: Schema.String.check(Schema.makeFilter((secret) => isTokenSecret(secret))),
34260
+ hostGrant: CredentialRef,
34261
+ cnf: Schema.optional(KeyBinding)
34262
+ });
34263
+ const decodePending = Schema.decodeUnknownOption(Schema.fromJsonString(PendingStart));
34264
+ const encodePending = Schema.encodeSync(Schema.fromJsonString(PendingStart));
34265
+ /** Whether a pending start was authorized as a start now would be: by this grant, with this key. */
34266
+ const sameAuthority = (pending, hostGrant, cnf) => pending.hostGrant.person === hostGrant.person && pending.hostGrant.id === hostGrant.id && pending.cnf?.jkt === cnf?.jkt;
33249
34267
  /** A secret's SHA-256, as hex: all ref0 is told of it. */
33250
34268
  const hashOf = (secret) => Effect.map(Effect.promise(async () => await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret))), (digest) => Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""));
33251
34269
  /** How the harness is named in a plain title. */
@@ -33266,19 +34284,27 @@ const whyNotSaidYet = "Not said yet: the agent says why this thread is needed, a
33266
34284
  const startHarnessSession = Effect.fnUntraced(function* (input) {
33267
34285
  const hostGrant = yield* HostGrant;
33268
34286
  const keychain = yield* Keychain;
33269
- const config = yield* Ref0Config;
33270
34287
  const harness = input.binding.harness === "codex" ? "codex" : "claude-code";
33271
34288
  const folder = yield* (yield* ConfigFolders).folder(harness);
33272
34289
  const grant = yield* hostGrant.get(folder);
33273
34290
  if (Option.isNone(grant) || grant.value.harness !== harness) return Option.none();
34291
+ const authorizing = grantRef(grant.value);
34292
+ if (Option.isNone(authorizing)) return Option.none();
34293
+ const cnf = grant.value.cnf;
34294
+ const client = yield* keptClient(grant.value);
33274
34295
  const pending = pendingAccount(input.binding);
33275
- const held = Option.flatMap(yield* keychain.get(sessionAccount(input.binding)), (kept) => parseToken(Redacted.value(kept)));
33276
- const resent = Option.filter(Option.flatMap(yield* keychain.get(pending), (kept) => decodePending(Redacted.value(kept))), (one) => !Option.exists(held, (parts) => parts.secret === one.secret));
34296
+ const held = Option.flatMap(readKeptSession(yield* keychain.get(sessionAccount(input.binding))), (kept) => parseToken(Redacted.value(kept.token)));
34297
+ const resent = Option.filter(Option.flatMap(yield* keychain.get(pending), (kept) => decodePending(Redacted.value(kept))), (one) => sameAuthority(one, authorizing.value, cnf) && !Option.exists(held, (parts) => parts.secret === one.secret));
33277
34298
  const { requestId, secret } = Option.isSome(resent) ? resent.value : {
33278
34299
  requestId: yield* newRequestId,
33279
34300
  secret: encodeSecret(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))
33280
34301
  };
33281
- if (Option.isNone(resent)) yield* keychain.set(pending, Redacted.make(`${requestId} ${secret}`));
34302
+ if (Option.isNone(resent)) yield* keychain.set(pending, Redacted.make(encodePending({
34303
+ requestId,
34304
+ secret,
34305
+ hostGrant: authorizing.value,
34306
+ cnf
34307
+ })));
33282
34308
  const hash = yield* hashOf(secret);
33283
34309
  const where = input.on.kind === "thread" ? threadParts(input.on.written) : {
33284
34310
  owner: grant.value.owner,
@@ -33305,7 +34331,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
33305
34331
  trigger: input.trigger,
33306
34332
  attended: "reported"
33307
34333
  });
33308
- const started = yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)))).sessions.start({
34334
+ const started = yield* client.sessions.start({
33309
34335
  headers: { "idempotency-key": requestId },
33310
34336
  payload
33311
34337
  }).pipe(Effect.tapError((error) => Match.value(error).pipe(Match.tag("ProjectNotFound", "ProjectNotReady", "ThreadNotFound", "ThreadIsClosed", "TextRefused", "NotPermitted", "DeliveryOutsideWindow", () => keychain.remove(pending)), Match.orElse(() => Effect.void))));
@@ -33313,11 +34339,20 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
33313
34339
  kind: "project",
33314
34340
  name: newThreadProject
33315
34341
  }, started.threadNumber);
34342
+ const kept = {
34343
+ token: Redacted.make(formatToken(started.credential.person, started.credential.id, secret)),
34344
+ session: started.session,
34345
+ hostGrant: authorizing.value,
34346
+ cnf
34347
+ };
33316
34348
  yield* keychain.remove(gitTokenAccount(input.binding)).pipe(Effect.ignore);
33317
- yield* keychain.set(sessionAccount(input.binding), Redacted.make(formatToken(started.credential.person, started.credential.id, secret)));
34349
+ yield* keychain.set(sessionAccount(input.binding), keptSessionValue(kept));
33318
34350
  yield* keychain.set(threadAccount(input.binding), Redacted.make(thread)).pipe(Effect.ignore);
33319
34351
  yield* keychain.remove(pending).pipe(Effect.ignore);
33320
- return Option.some(started);
34352
+ return Option.some({
34353
+ started,
34354
+ kept
34355
+ });
33321
34356
  });
33322
34357
  //#endregion
33323
34358
  //#region src/harness/watched-start.ts
@@ -33400,14 +34435,13 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
33400
34435
  const owned = yield* Ref.make(Option.none());
33401
34436
  const onTheWay = yield* Ref.make(Option.none());
33402
34437
  const listen = Effect.fnUntraced(function* (bound) {
33403
- const client = yield* clientOf(bound.token);
33404
34438
  let pause = 1;
33405
34439
  let opened = false;
33406
34440
  for (;;) {
33407
- const ticket = yield* client.live.ticket({ payload: { track: {
34441
+ const ticket = yield* clientOf(bound.kept).pipe(Effect.flatMap((client) => client.live.ticket({ payload: { track: {
33408
34442
  kind: "session",
33409
34443
  session: bound.session
33410
- } } }).pipe(Effect.option);
34444
+ } } })), Effect.option);
33411
34445
  let closed;
33412
34446
  if (Option.isSome(ticket)) {
33413
34447
  if (opened) yield* edges.moved;
@@ -33433,12 +34467,12 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
33433
34467
  }
33434
34468
  });
33435
34469
  const renew = Effect.fnUntraced(function* (bound) {
33436
- const client = yield* clientOf(bound.token);
33437
34470
  for (;;) {
33438
- const renewed = yield* client.sessions.renew({
34471
+ const key = yield* newRequestId;
34472
+ const renewed = yield* clientOf(bound.kept).pipe(Effect.flatMap((client) => client.sessions.renew({
33439
34473
  params: { session: bound.session },
33440
- headers: { "idempotency-key": yield* newRequestId }
33441
- }).pipe(Effect.as("renewed"), Effect.catchTag("SessionHasEnded", () => Effect.succeed("ended")), Effect.orElseSucceed(() => "failed"));
34474
+ headers: { "idempotency-key": key }
34475
+ }))).pipe(Effect.as("renewed"), Effect.catchTag("SessionHasEnded", () => Effect.succeed("ended")), Effect.orElseSucceed(() => "failed"));
33442
34476
  if (renewed === "ended") {
33443
34477
  yield* oneAtATime.withPermits(1)(Ref.set(state, {
33444
34478
  kind: "ended",
@@ -33450,9 +34484,9 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
33450
34484
  }
33451
34485
  });
33452
34486
  const keptThread = (binding) => keychain.get(threadAccount(binding)).pipe(Effect.map(Option.flatMap((kept) => decodeWrittenThread(Redacted.value(kept)))), Effect.orElseSucceed(() => Option.none()));
33453
- const hasEnded = (token) => clientOf(token).pipe(Effect.flatMap((client) => client.me.get()), Effect.as(false), Effect.catchTag("SessionHasEnded", () => Effect.succeed(true)), Effect.orElseSucceed(() => false));
34487
+ const hasEnded = (kept) => clientOf(kept).pipe(Effect.flatMap((client) => client.me.get()), Effect.as(false), Effect.catchTag("SessionHasEnded", () => Effect.succeed(true)), Effect.orElseSucceed(() => false));
33454
34488
  const heldFor = Effect.fnUntraced(function* (binding, over) {
33455
- const kept = yield* keychain.get(sessionAccount(binding)).pipe(Effect.orElseSucceed(() => Option.none()));
34489
+ const kept = readKeptSession(yield* keychain.get(sessionAccount(binding)).pipe(Effect.orElseSucceed(() => Option.none())));
33456
34490
  if (!over || Option.isNone(kept) || !(yield* hasEnded(kept.value))) return kept;
33457
34491
  return Option.none();
33458
34492
  });
@@ -33491,8 +34525,7 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
33491
34525
  "ProjectNotFound",
33492
34526
  "NotPermitted"
33493
34527
  ], (refused) => Option.isSome(before) ? Effect.andThen(keychain.remove(threadAccount(binding)).pipe(Effect.ignore), startOn(start.thread)) : Effect.fail(refused)), Effect.orElseSucceed(() => Option.none()));
33494
- if (Option.isNone(started)) return Option.none();
33495
- return yield* keychain.get(sessionAccount(binding)).pipe(Effect.orElseSucceed(() => Option.none()));
34528
+ return Option.map(started, (one) => one.kept);
33496
34529
  });
33497
34530
  const startedFor = Effect.fnUntraced(function* (binding, hook, starting) {
33498
34531
  const sent = yield* Ref.get(onTheWay);
@@ -33518,28 +34551,19 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
33518
34551
  if ((over && !starting || waiting) && !sent) return Option.none();
33519
34552
  const binding = yield* bindingForServer(hook.harnessSession);
33520
34553
  const held = Option.isNone(binding) ? Option.none() : yield* heldFor(binding.value, over);
33521
- const token = Option.isSome(held) || Option.isNone(binding) ? held : yield* startedFor(binding.value, hook, starting);
33522
- if (Option.isNone(binding) || Option.isNone(token)) {
34554
+ const kept = Option.isSome(held) || Option.isNone(binding) ? held : yield* startedFor(binding.value, hook, starting);
34555
+ if (Option.isNone(binding) || Option.isNone(kept)) {
33523
34556
  if (!over) yield* Ref.set(state, {
33524
34557
  kind: "unbound",
33525
34558
  until: now + unboundMillis
33526
34559
  });
33527
34560
  return Option.none();
33528
34561
  }
33529
- const client = yield* clientOf(token.value);
33530
- const me = yield* client.me.get();
33531
- if (me.session === void 0) {
33532
- yield* Ref.set(state, {
33533
- kind: "unbound",
33534
- until: now + unboundMillis
33535
- });
33536
- return Option.none();
33537
- }
33538
- const registered = yield* client.sessions.process({ params: { session: me.session.session } });
34562
+ const registered = yield* (yield* clientOf(kept.value)).sessions.process({ params: { session: kept.value.session } });
33539
34563
  const bound = {
33540
34564
  binding: binding.value,
33541
- token: token.value,
33542
- session: me.session.session,
34565
+ kept: kept.value,
34566
+ session: kept.value.session,
33543
34567
  generation: registered.generation,
33544
34568
  counter: 0,
33545
34569
  unreported: []
@@ -33635,175 +34659,53 @@ const sessionTasks = Effect.gen(function* () {
33635
34659
  };
33636
34660
  });
33637
34661
  //#endregion
33638
- //#region src/harness/turn-facts.ts
33639
- /** The most tool names a turn's record counts. */
33640
- const toolNames = 100;
33641
- /** The most replies a turn's tally remembers; a turn with more has its tokens unknown. */
33642
- const turnReplies = 1e4;
33643
- const nothingSeen = {
33644
- tools: /* @__PURE__ */ new Map(),
33645
- addresses: [],
33646
- more: 0,
33647
- model: void 0,
33648
- tokens: {
33649
- known: "waiting",
33650
- models: /* @__PURE__ */ new Map(),
33651
- replies: /* @__PURE__ */ new Map()
33652
- },
33653
- startedAt: void 0,
33654
- stoppedAt: void 0
33655
- };
33656
- /**
33657
- * A hook seen at `at`: a tool call counted by its tool once it ran, the files an edit is about to
33658
- * touch kept, at most 50 and the rest counted, the model, where the hook names one, and a `Stop`
33659
- * as when the agent last stopped.
33660
- */
33661
- function seeHook(seen, hook, edited, at) {
33662
- const tools = new Map(seen.tools);
33663
- const name = hook.tool?.name;
33664
- if (hook.event === "PostToolUse" && name !== void 0) {
33665
- const counted = tools.get(name);
33666
- if (counted !== void 0 || tools.size < toolNames) tools.set(name, (counted ?? 0) + 1);
33667
- }
33668
- const addresses = [...seen.addresses];
33669
- let more = seen.more;
33670
- for (const each of edited) {
33671
- if (addresses.some((kept) => kept.repo === each.repo && kept.path === each.path)) continue;
33672
- if (addresses.length < 50) addresses.push(each);
33673
- else more += 1;
33674
- }
33675
- return {
33676
- ...seen,
33677
- tools,
33678
- addresses,
33679
- more,
33680
- model: hook.model ?? seen.model,
33681
- stoppedAt: hook.event === "Stop" ? at : seen.stoppedAt
33682
- };
33683
- }
33684
- /**
33685
- * A read's replies added to a turn's, each by its message ID at the most any of its lines says:
33686
- * a subagent's transcript writes a reply's output as it grows, line by line, so a line of a reply
33687
- * already counted adds only what it grew. `own` when the read was of the session's own
33688
- * transcript, which makes the turn's tokens known; nothing makes them known once a read has
33689
- * failed.
33690
- */
33691
- function addReplies(tally, replies, own) {
33692
- if (tally.known === "failed") return tally;
33693
- const models = new Map(tally.models);
33694
- const counted = new Map(tally.replies);
33695
- for (const reply of replies) {
33696
- const before = counted.get(reply.id);
33697
- const most = {
33698
- model: before?.model ?? reply.model,
33699
- input: Math.max(before?.input ?? 0, reply.input),
33700
- output: Math.max(before?.output ?? 0, reply.output),
33701
- cacheWrite: Math.max(before?.cacheWrite ?? 0, reply.cacheWrite),
33702
- cacheRead: Math.max(before?.cacheRead ?? 0, reply.cacheRead)
33703
- };
33704
- const sum = models.get(most.model);
33705
- counted.set(reply.id, most);
33706
- models.set(most.model, {
33707
- input: (sum?.input ?? 0) + most.input - (before?.input ?? 0),
33708
- output: (sum?.output ?? 0) + most.output - (before?.output ?? 0),
33709
- cacheWrite: (sum?.cacheWrite ?? 0) + most.cacheWrite - (before?.cacheWrite ?? 0),
33710
- cacheRead: (sum?.cacheRead ?? 0) + most.cacheRead - (before?.cacheRead ?? 0)
33711
- });
33712
- }
33713
- if (counted.size > turnReplies) return readFailed(tally);
33714
- return {
33715
- known: own ? "read" : tally.known,
33716
- models,
33717
- replies: counted
33718
- };
33719
- }
33720
- /**
33721
- * A read's requests added to a turn's, by model, as Codex's rollout steps its running total.
33722
- * `own` as for `addReplies`: a rollout is always the session's own.
33723
- */
33724
- function addSteps(tally, steps, own) {
33725
- if (tally.known === "failed") return tally;
33726
- const models = new Map(tally.models);
33727
- for (const step of steps) {
33728
- const sum = models.get(step.model);
33729
- models.set(step.model, {
33730
- input: (sum?.input ?? 0) + step.input,
33731
- output: (sum?.output ?? 0) + step.output,
33732
- cacheWrite: (sum?.cacheWrite ?? 0) + step.cacheWrite,
33733
- cacheRead: (sum?.cacheRead ?? 0) + step.cacheRead
33734
- });
33735
- }
33736
- return {
33737
- ...tally,
33738
- known: own ? "read" : tally.known,
33739
- models
33740
- };
33741
- }
33742
- /** A read that failed: the turn's tokens are unknown from then on. */
33743
- const readFailed = (tally) => ({
33744
- ...tally,
33745
- known: "failed"
33746
- });
33747
- /** A read's replies and requests added to a turn's tokens, or its tokens unknown if it failed. */
33748
- const addRead = (tally, read, own) => read.whole ? addSteps(addReplies(tally, read.replies, own), read.steps, own) : readFailed(tally);
33749
- /**
33750
- * A turn's tokens as ref0 keeps them: each model's counts once the session's own transcript is
33751
- * read, or unknown, with no counts, where it wasn't, a read failed, or a count passes what a
33752
- * turn may hold.
33753
- */
33754
- function turnTokens(tally) {
33755
- if (tally.known !== "read" || tally.models.size > 16) return { known: "unknown" };
33756
- const models = [...tally.models].map(([model, counts]) => ({
33757
- model,
33758
- ...counts
33759
- }));
33760
- return models.every((each) => Math.max(each.input, each.output, each.cacheWrite, each.cacheRead) <= 1e8 && each.model.length >= 1 && each.model.length <= 128) ? {
33761
- known: "read",
33762
- models
33763
- } : { known: "unknown" };
33764
- }
33765
- /**
33766
- * A turn's facts as ref0 keeps them, with its stop as how long after ref0 started it the agent last
33767
- * stopped: none until both are seen, and 0 for a `Stop` before its start landed.
33768
- */
33769
- function turnFacts(seen) {
33770
- const facts = {
33771
- modelSource: seen.model === void 0 ? "unknown" : "hook",
33772
- tools: Object.fromEntries(seen.tools),
33773
- addresses: seen.addresses,
33774
- more: seen.more,
33775
- tokens: turnTokens(seen.tokens)
33776
- };
33777
- if (seen.model !== void 0) facts.model = seen.model;
33778
- if (seen.startedAt !== void 0 && seen.stoppedAt !== void 0) facts.stoppedAfterMs = Math.max(seen.stoppedAt - seen.startedAt, 0);
33779
- return facts;
33780
- }
33781
- //#endregion
33782
34662
  //#region src/harness/session-turns.ts
33783
34663
  /** The open turn as `SessionEnd` tells it once more: none until ref0 has numbered it. */
33784
34664
  const keptOf = (now) => now.kind === "open" ? Option.some({
33785
34665
  number: now.number,
33786
34666
  facts: turnFacts(now.seen),
33787
- tokens: now.seen.tokens
34667
+ reads: now.seen,
34668
+ startedAt: now.seen.startedAt
33788
34669
  }) : Option.none();
33789
34670
  /**
33790
- * The transcripts a hook's read takes, and whether the session's own is among them: at the
33791
- * tracked turn's own `Stop`, the session's; at any subagent's `SubagentStop`, its own, whichever
33792
- * turn started it, since it adds to the turn still open, with the turn Codex names there, the
33793
- * subagent's own, where its rollout's own part starts. A new turn's prompt reads the session's
33794
- * first, for the turn it ends.
34671
+ * The transcripts a hook's read takes, and whose they are: at the tracked turn's own `Stop`, the
34672
+ * session's; at any subagent's `SubagentStop`, its own, whichever turn started it, since it adds
34673
+ * to the turn still open, with the turn Codex names there, the subagent's own, where its
34674
+ * rollout's own part starts, and its type. A new turn's prompt reads the session's first, for
34675
+ * the turn it ends.
33795
34676
  */
33796
34677
  const toRead = (hook, ownHook) => {
33797
34678
  if (hook.event === "Stop" && ownHook && hook.transcript !== void 0) return {
33798
34679
  transcripts: [hook.transcript],
33799
- own: true
34680
+ of: { own: true }
33800
34681
  };
33801
34682
  if (hook.event === "SubagentStop" && hook.agentTranscript !== void 0) return {
33802
34683
  transcripts: [hook.agentTranscript],
33803
- own: false,
34684
+ of: {
34685
+ own: false,
34686
+ subagent: hook.agentTranscript,
34687
+ type: hook.agentType
34688
+ },
33804
34689
  turn: { since: hook.harnessTurn }
33805
34690
  };
33806
34691
  };
34692
+ /** A read that couldn't happen, as of a subagent whose harness kept no transcript of it. */
34693
+ const unread = {
34694
+ replies: [],
34695
+ steps: [],
34696
+ compactions: [],
34697
+ whole: false
34698
+ };
34699
+ /**
34700
+ * A subagent's `SubagentStop` that names no transcript of it, as Codex's in a session kept
34701
+ * nowhere: the subagent counted by its ID, with its type, and its tokens, and so the turn's,
34702
+ * unknown, since what it used can't be read.
34703
+ */
34704
+ const untranscribed = (hook) => hook.event === "SubagentStop" && hook.agentTranscript === void 0 ? {
34705
+ own: false,
34706
+ subagent: `agent:${hook.agent ?? ""}`,
34707
+ type: hook.agentType
34708
+ } : void 0;
33807
34709
  /** A harness session's turns, each numbered by ref0 as it starts. */
33808
34710
  const sessionTurns = Effect.fnUntraced(function* (edges) {
33809
34711
  const transcripts = yield* Transcripts;
@@ -33824,7 +34726,11 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33824
34726
  const ending = prompted === void 0 ? void 0 : yield* transcripts.read(owner, harness, [prompted], { until: harnessTurn });
33825
34727
  const fresh = late && harness === "claude-code" ? {
33826
34728
  ...nothingSeen,
33827
- tokens: readFailed(nothingSeen.tokens)
34729
+ tokens: readFailed(nothingSeen.tokens),
34730
+ context: {
34731
+ ...nothingSeen.context,
34732
+ known: "failed"
34733
+ }
33828
34734
  } : nothingSeen;
33829
34735
  const tracked = isNew ? Option.some({
33830
34736
  kind: "starting",
@@ -33833,7 +34739,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33833
34739
  turn: now.number,
33834
34740
  seen: ending === void 0 ? now.seen : {
33835
34741
  ...now.seen,
33836
- tokens: addRead(now.seen.tokens, ending, true)
34742
+ ...addRead(now.seen, ending, { own: true })
33837
34743
  }
33838
34744
  }) : now.previous),
33839
34745
  seen: fresh
@@ -33845,9 +34751,13 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33845
34751
  const hooked = ownHook && !isNew ? seeHook(tracked.value.seen, hook, edited, yield* Clock.currentTimeMillis) : tracked.value.seen;
33846
34752
  const reading = toRead(hook, ownHook);
33847
34753
  const read = reading === void 0 ? void 0 : yield* transcripts.read(owner, harness, reading.transcripts, reading.turn);
33848
- const seen = read === void 0 || reading === void 0 ? hooked : {
34754
+ const unseen = untranscribed(hook);
34755
+ const seen = read === void 0 || reading === void 0 ? unseen === void 0 ? hooked : {
34756
+ ...hooked,
34757
+ ...addRead(hooked, unread, unseen)
34758
+ } : {
33849
34759
  ...hooked,
33850
- tokens: addRead(hooked.tokens, read, reading.own)
34760
+ ...addRead(hooked, read, reading.of)
33851
34761
  };
33852
34762
  const places = (read ?? ending)?.places;
33853
34763
  let now = {
@@ -33856,7 +34766,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33856
34766
  };
33857
34767
  yield* Effect.uninterruptible(Effect.gen(function* () {
33858
34768
  yield* Ref.set(turn, Option.some(now));
33859
- if (places !== void 0) yield* transcripts.keep(owner, places, keptOf(now));
34769
+ if (places !== void 0 || unseen !== void 0) yield* transcripts.keep(owner, places ?? /* @__PURE__ */ new Map(), keptOf(now));
33860
34770
  }));
33861
34771
  if (now.kind === "starting") {
33862
34772
  if (Option.isSome(now.previous)) yield* edges.tasksLanded(client, bound, deadline);
@@ -33907,7 +34817,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33907
34817
  if (ownHook && hook.event === "Stop") {
33908
34818
  yield* edges.tasksLanded(client, bound, deadline);
33909
34819
  yield* tellFacts;
33910
- } else if (hook.event === "SubagentStop" && read !== void 0) yield* tellFacts;
34820
+ } else if (hook.event === "SubagentStop" && (read !== void 0 || unseen !== void 0)) yield* tellFacts;
33911
34821
  });
33912
34822
  return {
33913
34823
  now: Ref.get(turn),
@@ -34038,7 +34948,6 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
34038
34948
  */
34039
34949
  static layerFor(agent) {
34040
34950
  return Layer.effect(HarnessSession, Effect.gen(function* () {
34041
- const config = yield* Ref0Config;
34042
34951
  const context = yield* Effect.context();
34043
34952
  const state = yield* Ref.make({
34044
34953
  kind: "unbound",
@@ -34058,13 +34967,13 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
34058
34967
  within: Effect.suspend(() => binding.within),
34059
34968
  scope
34060
34969
  });
34061
- const clientOf = (token) => ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)));
34970
+ const clientOf = (kept) => keptClient(kept).pipe(Effect.provide(context));
34062
34971
  const watch = yield* baseWatch({
34063
34972
  scope,
34064
34973
  clientOf
34065
34974
  });
34066
34975
  const settle = Effect.fnUntraced(function* (bound) {
34067
- const client = yield* clientOf(bound.token);
34976
+ const client = yield* clientOf(bound.kept);
34068
34977
  const refused = () => Effect.succeed("refused");
34069
34978
  yield* attribution.settle(ownerOf(bound), (repo, part) => client.sessions.commit({
34070
34979
  params: {
@@ -34110,7 +35019,7 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
34110
35019
  if (hook.event === "UserPromptSubmit") yield* Ref.set(prompted, Option.some(hook));
34111
35020
  const bound = yield* binding.bind(hook);
34112
35021
  if (Option.isNone(bound)) return Option.none();
34113
- const client = yield* clientOf(bound.value.token);
35022
+ const client = yield* clientOf(bound.value.kept);
34114
35023
  const prompt = Option.filter(yield* Ref.get(prompted), (one) => one.harnessSession === hook.harnessSession && one.harnessTurn === hook.harnessTurn);
34115
35024
  if (Option.isSome(prompt)) yield* Ref.set(prompted, Option.none());
34116
35025
  if (Option.isSome(prompt) && prompt.value !== hook) yield* turns.trackLate(client, bound.value, prompt.value, deadline);
@@ -34311,7 +35220,7 @@ const endWithin = "5 seconds";
34311
35220
  const lastReadWithin = "2 seconds";
34312
35221
  /**
34313
35222
  * The turn `ref0 mcp` last told ref0 of, told once more with what the session's transcript
34314
- * gained since: a reply written after its last `Stop` counts in it, and a `Stop` whose facts
35223
+ * gained since: a reply or a compaction written after its last `Stop` counts in it, and a `Stop` whose facts
34315
35224
  * didn't land has them sent again. Nothing for a turn ref0 hasn't numbered.
34316
35225
  */
34317
35226
  const lastRead = Effect.fnUntraced(function* (client, session, harness, transcript) {
@@ -34324,7 +35233,7 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
34324
35233
  };
34325
35234
  const turn = kept.value.turn.value;
34326
35235
  const read = transcript === null || transcript === void 0 || transcript === "" ? void 0 : yield* transcripts.read(owner, harness, [transcript]);
34327
- const tokens = read === void 0 ? turn.tokens : addRead(turn.tokens, read, true);
35236
+ const reads = read === void 0 ? turn.reads : addRead(turn.reads, read, { own: true });
34328
35237
  yield* client.sessions.turnFacts({
34329
35238
  params: {
34330
35239
  session,
@@ -34334,16 +35243,11 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
34334
35243
  generation: owner.generation,
34335
35244
  facts: {
34336
35245
  ...turn.facts,
34337
- tokens: turnTokens(tokens)
35246
+ ...readFacts(reads, turn.startedAt)
34338
35247
  }
34339
35248
  }
34340
35249
  });
34341
35250
  });
34342
- /** The typed `/v1` as a session, with its own token: not the one this process's environment names. */
34343
- const asSession = (token) => Effect.gen(function* () {
34344
- const config = yield* Ref0Config;
34345
- return yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)));
34346
- });
34347
35251
  /** What the hook does, from the hook's input on stdin. */
34348
35252
  const endHarnessSession = Effect.gen(function* () {
34349
35253
  const stdio = yield* Stdio.Stdio;
@@ -34355,13 +35259,13 @@ const endHarnessSession = Effect.gen(function* () {
34355
35259
  const binding = yield* bindingHere(hook.value.session_id);
34356
35260
  if (Option.isNone(binding)) return;
34357
35261
  const account = sessionAccount(binding.value);
34358
- const token = yield* keychain.get(account);
34359
- if (Option.isNone(token)) return;
35262
+ const held = yield* keychain.get(account);
35263
+ if (Option.isNone(held)) return;
34360
35264
  yield* Effect.gen(function* () {
34361
- const client = yield* asSession(token.value);
34362
- const me = yield* client.me.get();
34363
- if (me.session === void 0) return;
34364
- const session = me.session.session;
35265
+ const kept = readKeptSession(held);
35266
+ if (Option.isNone(kept)) return;
35267
+ const client = yield* keptClient(kept.value);
35268
+ const session = kept.value.session;
34365
35269
  const harness = binding.value.harness === "codex" ? "codex" : "claude-code";
34366
35270
  yield* lastRead(client, session, harness, hook.value.transcript_path).pipe(Effect.timeout(lastReadWithin), Effect.ignore);
34367
35271
  yield* client.sessions.end({
@@ -34558,23 +35462,19 @@ const setCommitEnvironment = Effect.fnUntraced(function* (session) {
34558
35462
  */
34559
35463
  const decodeHook = Schema.decodeUnknownOption(Schema.fromJsonString(SessionStartHookInput));
34560
35464
  /**
34561
- * What blocks the session's thread, in the words its agent reads, read with the session's own
34562
- * token; none if nothing does. A session already started is found by its token.
35465
+ * What blocks the session's thread, in the words its agent reads, read as the session, as it's
35466
+ * kept; none if nothing does.
34563
35467
  */
34564
- const blockedBy = Effect.fnUntraced(function* (token, known) {
34565
- const config = yield* Ref0Config;
34566
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)));
34567
- const session = Option.isSome(known) ? known.value : (yield* client.me.get()).session?.session;
34568
- if (session === void 0) return Option.none();
34569
- const read = yield* client.sessions.blockers({
34570
- params: { session },
35468
+ const blockedBy = Effect.fnUntraced(function* (kept) {
35469
+ const read = yield* (yield* keptClient(kept)).sessions.blockers({
35470
+ params: { session: kept.session },
34571
35471
  query: {}
34572
35472
  });
34573
35473
  if (read.blockers.length === 0) return Option.none();
34574
35474
  return Option.some(renderBlockedBy(read.thread, read.blockers));
34575
35475
  });
34576
35476
  /** Says what blocks the session's thread, if anything does; says nothing if ref0 can't tell. */
34577
- const sayBlockers = (token, known) => blockedBy(token, known).pipe(Effect.flatMap(Option.match({
35477
+ const sayBlockers = (kept) => blockedBy(kept).pipe(Effect.flatMap(Option.match({
34578
35478
  onNone: () => Effect.void,
34579
35479
  onSome: Console.log
34580
35480
  })), Effect.ignore);
@@ -34615,9 +35515,8 @@ const startOnHook = (protocol) => Effect.gen(function* () {
34615
35515
  const why = kept.readable ? `no agent is connected for this ${name} config folder, ${folder}` : `the host grant kept for this ${name} config folder, ${folder}, can't be read`;
34616
35516
  return yield* Console.log(`ref0: ${why}, so no session starts. In your own terminal, run: ${repair}`);
34617
35517
  }
34618
- const account = sessionAccount(binding.value);
34619
- const held = yield* keychain.get(account);
34620
- if (Option.isSome(held)) return yield* sayBlockers(held.value, Option.none());
35518
+ const held = readKeptSession(yield* keychain.get(sessionAccount(binding.value)));
35519
+ if (Option.isSome(held)) return yield* sayBlockers(held.value);
34621
35520
  const place = startPlace(yield* checkout.threadAt(hook.value.cwd ?? "."), yield* watchedHere);
34622
35521
  if (Option.isNone(place.thread)) return;
34623
35522
  const thread = place.thread.value;
@@ -34635,11 +35534,10 @@ const startOnHook = (protocol) => Effect.gen(function* () {
34635
35534
  version
34636
35535
  }).pipe(Effect.tapError((error) => place.trigger.kind === "event" ? Console.log(refusedWords(thread, "nextStep" in error ? error.nextStep : void 0)) : Effect.void));
34637
35536
  if (Option.isNone(started)) return;
34638
- yield* Console.log(`ref0: this session is on ${thread}, as its session ${started.value.sessionNumber}.`);
35537
+ yield* Console.log(`ref0: this session is on ${thread}, as its session ${started.value.started.sessionNumber}.`);
34639
35538
  const watched = watchedWords(place, thread);
34640
35539
  if (watched !== void 0) yield* Console.log(`ref0: ${watched}`);
34641
- const token = yield* keychain.get(account);
34642
- if (Option.isSome(token)) yield* sayBlockers(token.value, Option.some(started.value.session));
35540
+ yield* sayBlockers(started.value.kept);
34643
35541
  }).pipe(Effect.timeout("10 seconds"), Effect.ignore);
34644
35542
  const sessionStart = Command.make(harnessCommandNames.sessionStart, { protocol: protocolFlag }, (flags) => startOnHook(flags.protocol)).pipe(Command.withDescription("Starts the ref0 session of a harness session that’s opening"), Command.withShortDescription("Start a ref0 session for the harness"));
34645
35543
  /** The commands a harness runs: `ref0 mcp`, and its command hooks. */
@@ -34647,7 +35545,7 @@ const harnessCommands = [mcp, Command.make(harnessCommandNames.hook).pipe(Comman
34647
35545
  //#endregion
34648
35546
  //#region src/harness/command-token.ts
34649
35547
  /**
34650
- * The session an agent's command acts as: its harness session's, by the token kept under its
35548
+ * The session an agent's command acts as: its harness session's, by the record kept under its
34651
35549
  * binding. Outside a harness there's none, and a command is its person's, asked by approval; so
34652
35550
  * too in a harness whose config folder has no agent connected. Where one is connected, a harness
34653
35551
  * session with no token has lost its start, and the command says why, as far as ref0 can tell,
@@ -34695,11 +35593,12 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
34695
35593
  const current = Effect.gen(function* () {
34696
35594
  const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
34697
35595
  if (Option.isNone(binding)) return Option.none();
34698
- const kept = yield* keychain.get(sessionAccount(binding.value));
35596
+ const held = yield* keychain.get(sessionAccount(binding.value));
35597
+ const kept = readKeptSession(held);
34699
35598
  if (Option.isSome(kept)) return kept;
34700
35599
  const name = binding.value.harness === "codex" ? "codex" : "claude-code";
34701
35600
  const grant = yield* hostGrant.get(yield* folders.folder(name)).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
34702
- if (Option.isNone(grant) || grant.value.harness !== name) return Option.none();
35601
+ if ((Option.isNone(grant) || grant.value.harness !== name) && Option.isNone(held)) return Option.none();
34703
35602
  const pending = yield* keychain.get(pendingAccount(binding.value));
34704
35603
  const serverRuns = yield* servers.runsUnder(binding.value.process);
34705
35604
  return yield* new NoSession({ nextStep: noSessionStep(name, `plugin:${plugin}:${plugin}`, {
@@ -34881,25 +35780,33 @@ const wordsOf = (report, home) => {
34881
35780
  }, ...check.next === void 0 ? [] : [{ command: check.next }]])];
34882
35781
  };
34883
35782
  const refused = () => Effect.succeed({ kind: "refused" });
34884
- const whoIs = Effect.fnUntraced(function* (token) {
34885
- const config = yield* Ref0Config;
34886
- return yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)))).me.get().pipe(Effect.timeout("10 seconds"), Effect.map((me) => ({
34887
- kind: "accepted",
34888
- me
34889
- })), Effect.catchTags({
34890
- Unauthenticated: refused,
34891
- SessionHasEnded: refused
34892
- }), Effect.orElseSucceed(() => ({ kind: "unanswered" })));
34893
- });
35783
+ const unsigned = (failed) => Effect.succeed({
35784
+ kind: "unsigned",
35785
+ nextStep: failed.nextStep
35786
+ });
35787
+ /** Asks ref0 who a client's credential is. */
35788
+ const whoIs = (client) => client.me.get().pipe(Effect.timeout("10 seconds"), Effect.map((me) => ({
35789
+ kind: "accepted",
35790
+ me
35791
+ })), Effect.catchTags({
35792
+ Unauthenticated: refused,
35793
+ SessionHasEnded: refused,
35794
+ InvalidDpopProof: refused,
35795
+ KeyStoreUnavailable: unsigned
35796
+ }), Effect.orElseSucceed(() => ({ kind: "unanswered" })));
34894
35797
  const unansweredWords = (api) => `ref0 didn’t answer at ${api}.`;
34895
35798
  /** Check the grant kept for this folder, without borrowing another folder's authority. */
34896
35799
  const grantChecks = Effect.fnUntraced(function* (grant, configFolder) {
34897
- const asked = yield* whoIs(grant.token);
35800
+ const asked = yield* keptClient(grant).pipe(Effect.flatMap(whoIs), Effect.catchTags({
35801
+ InvalidDpopProof: refused,
35802
+ KeychainError: unsigned
35803
+ }));
34898
35804
  const connected = checkOf("connected", "yes", "this machine holds its grant");
34899
35805
  switch (asked.kind) {
34900
35806
  case "accepted": return [connected, checkOf("accepted", "yes", "ref0 accepts its grant")];
34901
35807
  case "refused": return [connected, checkOf("accepted", "no", "ref0 refuses its grant", yield* connectLineFor(grant.harness, configFolder))];
34902
35808
  case "unanswered": return [connected, checkOf("accepted", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status")];
35809
+ case "unsigned": return [connected, checkOf("accepted", "unknown", "this machine’s key couldn’t sign to ask", asked.nextStep)];
34903
35810
  }
34904
35811
  });
34905
35812
  /** Protocol mismatches either update this CLI or reinstall this folder's plugin. */
@@ -34990,12 +35897,17 @@ const sessionCheck = Effect.fnUntraced(function* (harness, configFolder) {
34990
35897
  if (Option.isNone(binding)) return checkOf("session", "unknown", (yield* harnessEnvironment).CODEX_SANDBOX === void 0 ? "no current session to check" : "Codex’s sandbox hides its session", launch);
34991
35898
  const token = yield* Effect.result((yield* Keychain).get(sessionAccount(binding.value)));
34992
35899
  if (Result.isFailure(token)) return checkOf("session", "unknown", `The keychain didn’t say whether ref0 follows this ${configFolders[harness].name} session.`, token.failure.nextStep);
34993
- if (Option.isNone(token.success)) return checkOf("session", "no", "start a new session in this folder", launch);
34994
- const asked = yield* whoIs(token.success.value);
35900
+ const kept = readKeptSession(token.success);
35901
+ if (Option.isNone(kept)) return checkOf("session", "no", "start a new session in this folder", launch);
35902
+ const asked = yield* keptClient(kept.value).pipe(Effect.flatMap(whoIs), Effect.catchTags({
35903
+ InvalidDpopProof: refused,
35904
+ KeychainError: unsigned
35905
+ }));
34995
35906
  switch (asked.kind) {
34996
35907
  case "accepted": return asked.me.session === void 0 ? checkOf("session", "no", "start a new session in this folder", launch) : checkOf("session", "yes", `ref0 follows this session as ${asked.me.session.session}`);
34997
35908
  case "refused": return checkOf("session", "no", "ref0 no longer follows this session", launch);
34998
- case "unanswered": return checkOf("session", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status");
35909
+ case "unanswered":
35910
+ case "unsigned": return checkOf("session", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status");
34999
35911
  }
35000
35912
  });
35001
35913
  const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ json: asJson }) => Effect.gen(function* () {
@@ -35088,6 +36000,7 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
35088
36000
  ]));
35089
36001
  const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
35090
36002
  const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
36003
+ const machineKey = MachineKey.layer.pipe(Layer.provide(keychain));
35091
36004
  const sessionToken = harnessSessionToken.pipe(Layer.provide([
35092
36005
  keychain,
35093
36006
  harnessProcess,
@@ -35098,10 +36011,27 @@ const sessionToken = harnessSessionToken.pipe(Layer.provide([
35098
36011
  const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
35099
36012
  Ref0Config.layer,
35100
36013
  sessionToken,
35101
- FetchHttpClient.layer
35102
- ])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
36014
+ FetchHttpClient.layer,
36015
+ machineKey,
36016
+ ProofClock.layer
36017
+ ])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, machineKey, ProofClock.layer, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([
36018
+ Ref0Config.layer,
36019
+ FetchHttpClient.layer,
36020
+ machineKey,
36021
+ ProofClock.layer
36022
+ ])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
36023
+ //#endregion
36024
+ //#region src/root.ts
36025
+ /**
36026
+ * The CLI's root, around every command and the services it's given. A request's error holds the
36027
+ * request it was sent with, and a request a key signed holds its proof, which is good for a
36028
+ * minute, so `dpop` is redacted wherever one is printed, as `authorization` is. A failure no
36029
+ * command said, a service that couldn't be built included, is logged here, inside that redaction,
36030
+ * rather than by `runMain`, which logs outside it.
36031
+ */
36032
+ const atRoot = (services) => (program) => program.pipe(Effect.provide(services), Effect.tapCause((cause) => Cause.hasInterruptsOnly(cause) || !Runtime.getErrorReported(Cause.squash(cause)) ? Effect.void : Effect.logError(cause)), Effect.provideServiceEffect(Headers.CurrentRedactedNames, Effect.map(Effect.service(Headers.CurrentRedactedNames), (names) => [...names, "dpop"])));
35103
36033
  //#endregion
35104
36034
  //#region src/main.ts
35105
- ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);
36035
+ ref0.pipe(Command.run({ version: cliVersion }), atRoot(services), NodeRuntime.runMain({ disableErrorReporting: true }));
35106
36036
  //#endregion
35107
36037
  export {};