@ref0/cli 0.12.0 → 0.13.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 +1441 -592
  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 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;
@@ -5371,8 +5657,26 @@ Schema.Union([
5371
5657
  NotPermitted,
5372
5658
  DeliveryOutsideWindow
5373
5659
  ]);
5660
+ /**
5661
+ * Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
5662
+ * project's visibility, which a session's routes read who sees the project by.
5663
+ */
5664
+ const SessionPlace = Schema.Struct({
5665
+ owner: OwnerId,
5666
+ project: ProjectId,
5667
+ visibility: Visibility,
5668
+ thread: ThreadId,
5669
+ threadNumber: ThreadNumber,
5670
+ session: SessionId,
5671
+ number: SessionNumber,
5672
+ /** Whether it's still going: not ended, and its lease not run out. */
5673
+ live: Schema.Boolean
5674
+ });
5374
5675
  Schema.Struct({ session: SessionId });
5375
- Schema.Struct({ live: Schema.Boolean });
5676
+ Schema.Union([Schema.Struct({
5677
+ live: Schema.Literal(true),
5678
+ place: SessionPlace
5679
+ }), Schema.Struct({ live: Schema.Literal(false) })]);
5376
5680
  Schema.Struct({
5377
5681
  ...commandFields,
5378
5682
  project: ProjectId,
@@ -6379,8 +6683,8 @@ var ApprovalStale = class extends Schema.TaggedError()("ApprovalStale", { nextSt
6379
6683
  var PreviewNotReady = class extends Schema.TaggedError()("PreviewNotReady", { nextStep: Schema.String }, { httpApiStatus: 503 }) {};
6380
6684
  //#endregion
6381
6685
  //#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)) });
6686
+ const Base64Url$1 = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]*$/));
6687
+ Schema.Struct({ challenge: Base64Url$1.check(Schema.isMinLength(1)) });
6384
6688
  /** How a browser reaches a passkey's authenticator, as WebAuthn names it. */
6385
6689
  const PasskeyTransport = Schema.Literals([
6386
6690
  "ble",
@@ -6392,7 +6696,7 @@ const PasskeyTransport = Schema.Literals([
6392
6696
  "usb"
6393
6697
  ]);
6394
6698
  const CredentialDescriptor = Schema.Struct({
6395
- id: Base64Url,
6699
+ id: Base64Url$1,
6396
6700
  type: Schema.Literal("public-key"),
6397
6701
  transports: Schema.optionalKey(Schema.Array(PasskeyTransport))
6398
6702
  });
@@ -6403,11 +6707,11 @@ const PasskeyCreationOptions = Schema.Struct({
6403
6707
  name: Schema.String
6404
6708
  }),
6405
6709
  user: Schema.Struct({
6406
- id: Base64Url,
6710
+ id: Base64Url$1,
6407
6711
  name: Schema.String,
6408
6712
  displayName: Schema.String
6409
6713
  }),
6410
- challenge: Base64Url,
6714
+ challenge: Base64Url$1,
6411
6715
  pubKeyCredParams: Schema.Array(Schema.Struct({
6412
6716
  type: Schema.Literal("public-key"),
6413
6717
  alg: Schema.Int
@@ -6446,23 +6750,23 @@ const PasskeyCreationOptions = Schema.Struct({
6446
6750
  * as its WebAuthn library leaves it.
6447
6751
  */
6448
6752
  const PasskeyCreated = Schema.Struct({
6449
- id: Base64Url,
6450
- rawId: Base64Url,
6753
+ id: Base64Url$1,
6754
+ rawId: Base64Url$1,
6451
6755
  type: Schema.Literal("public-key"),
6452
6756
  response: Schema.Struct({
6453
- clientDataJSON: Base64Url,
6454
- attestationObject: Base64Url,
6455
- authenticatorData: Schema.optional(Base64Url),
6757
+ clientDataJSON: Base64Url$1,
6758
+ attestationObject: Base64Url$1,
6759
+ authenticatorData: Schema.optional(Base64Url$1),
6456
6760
  transports: Schema.optional(Schema.Array(PasskeyTransport)),
6457
6761
  publicKeyAlgorithm: Schema.optional(Schema.Int),
6458
- publicKey: Schema.optional(Base64Url)
6762
+ publicKey: Schema.optional(Base64Url$1)
6459
6763
  }),
6460
6764
  authenticatorAttachment: Schema.optional(Schema.Literals(["platform", "cross-platform"])),
6461
6765
  clientExtensionResults: Schema.Struct({ credProps: Schema.optionalKey(Schema.Struct({ rk: Schema.optionalKey(Schema.Boolean) })) })
6462
6766
  });
6463
6767
  /** What ref0 hands a browser to answer with a passkey: WebAuthn's request options, as JSON. */
6464
6768
  const PasskeyRequestOptions = Schema.Struct({
6465
- challenge: Base64Url,
6769
+ challenge: Base64Url$1,
6466
6770
  timeout: Schema.optionalKey(Schema.Int),
6467
6771
  rpId: Schema.optionalKey(Schema.String),
6468
6772
  allowCredentials: Schema.optionalKey(Schema.Array(CredentialDescriptor)),
@@ -6482,14 +6786,14 @@ const PasskeyRequestOptions = Schema.Struct({
6482
6786
  * as JSON. A field the browser doesn't have may be missing, or there as `undefined`.
6483
6787
  */
6484
6788
  const PasskeyAsserted = Schema.Struct({
6485
- id: Base64Url,
6486
- rawId: Base64Url,
6789
+ id: Base64Url$1,
6790
+ rawId: Base64Url$1,
6487
6791
  type: Schema.Literal("public-key"),
6488
6792
  response: Schema.Struct({
6489
- clientDataJSON: Base64Url,
6490
- authenticatorData: Base64Url,
6491
- signature: Base64Url,
6492
- userHandle: Schema.optional(Base64Url)
6793
+ clientDataJSON: Base64Url$1,
6794
+ authenticatorData: Base64Url$1,
6795
+ signature: Base64Url$1,
6796
+ userHandle: Schema.optional(Base64Url$1)
6493
6797
  }),
6494
6798
  authenticatorAttachment: Schema.optional(Schema.Literals(["platform", "cross-platform"])),
6495
6799
  clientExtensionResults: Schema.Struct({})
@@ -6543,10 +6847,15 @@ const RequestFolder = Schema.Struct({
6543
6847
  secretHash: SecretHash,
6544
6848
  replaces: Schema.optionalKey(TokenId)
6545
6849
  });
6546
- /** One to eight config folders, each with its own grant secret. */
6850
+ /**
6851
+ * One to eight config folders, each with its own grant secret, and the machine key its grants are
6852
+ * bound to: its public half, which a proof that key signed comes with. A request without one makes
6853
+ * grants bound to no key, as an older ref0 asks, until binding is required.
6854
+ */
6547
6855
  const RequestAttachment = Schema.Struct({
6548
6856
  machine: MachineId,
6549
6857
  machineName: MachineName,
6858
+ publicKey: Schema.optionalKey(PublicJwk),
6550
6859
  folders: Schema.Array(RequestFolder).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
6551
6860
  collectHash: SecretHash,
6552
6861
  linkHash: SecretHash,
@@ -6660,12 +6969,15 @@ const RequestOrganization = Schema.Struct({
6660
6969
  /**
6661
6970
  * The result collected with the machine's two proofs: each included folder's identity and grant,
6662
6971
  * its agent's owner name and organizations, and excluded paths. Confirmation checks each saved
6663
- * grant's live deadline in the Owner DO.
6972
+ * grant's live deadline in the Owner DO. Once a yes connected and the machine hasn't sent its
6973
+ * answer code, waiting says until when: `confirmBy`, when ref0 revokes each grant the machine
6974
+ * hasn't confirmed.
6664
6975
  */
6665
6976
  const RequestState = Schema.Union([
6666
6977
  Schema.Struct({
6667
6978
  state: Schema.Literal("waiting"),
6668
- expiresAt: Timestamp
6979
+ expiresAt: Timestamp,
6980
+ confirmBy: Schema.optionalKey(Timestamp)
6669
6981
  }),
6670
6982
  Schema.Struct({
6671
6983
  state: Schema.Literal("connected"),
@@ -6737,6 +7049,7 @@ Schema.Struct({
6737
7049
  request: ApprovalId,
6738
7050
  machine: MachineId,
6739
7051
  machineName: MachineName,
7052
+ cnf: Schema.optionalKey(KeyBinding),
6740
7053
  browserSession: TokenId,
6741
7054
  personGeneration: Generation,
6742
7055
  folders: Schema.Array(ConnectFolderCommand).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
@@ -10342,45 +10655,6 @@ Schema.Union([Schema.Struct({
10342
10655
  found: Schema.Literal(true),
10343
10656
  thread: ThreadId
10344
10657
  }), Schema.Struct({ found: Schema.Literal(false) })]);
10345
- /**
10346
- * ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
10347
- * credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
10348
- * 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
10349
- * it too. It's kept as written, case and any default port too: every path here is appended to it
10350
- * as it is.
10351
- */
10352
- 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"));
10353
- /** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
10354
- const signInBase = "/auth";
10355
- /**
10356
- * A route under `signInBase`, by its method: its `path` from the origin's root, as a request
10357
- * names it, and its `pathUnderBase`, as Better Auth names its endpoints.
10358
- */
10359
- const signInRoute = (method, pathUnderBase) => ({
10360
- method,
10361
- pathUnderBase,
10362
- path: `${signInBase}${pathUnderBase}`
10363
- });
10364
- /** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
10365
- const signInRoutes = {
10366
- gitHubSignIn: signInRoute("POST", "/sign-in/social"),
10367
- gitHubCallback: signInRoute("GET", "/callback/github"),
10368
- signOut: signInRoute("POST", "/sign-out")
10369
- };
10370
- /**
10371
- * The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
10372
- * token endpoint, registering a public client and revoking a token.
10373
- */
10374
- const oauthRoutes = {
10375
- metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
10376
- authorize: signInRoute("GET", "/oauth2/authorize"),
10377
- token: signInRoute("POST", "/oauth2/token"),
10378
- register: signInRoute("POST", "/oauth2/register"),
10379
- revoke: signInRoute("POST", "/oauth2/revoke")
10380
- };
10381
- signInRoute("POST", "/oauth2/consent");
10382
- [...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
10383
- `${oauthRoutes.metadata.pathUnderBase}`;
10384
10658
  //#endregion
10385
10659
  //#region ../contracts/src/live-calls.ts
10386
10660
  /**
@@ -11029,22 +11303,75 @@ const TurnTokens = Schema.Union([Schema.Struct({
11029
11303
  known: Schema.Literal("read"),
11030
11304
  models: Schema.Array(ModelTokens).check(Schema.isMaxLength(16), Schema.makeFilter((models) => new Set(models.map((each) => each.model)).size === models.length || "each model once"))
11031
11305
  }), Schema.Struct({ known: Schema.Literal("unknown") })]);
11306
+ const Name = Schema.String.check(Schema.isBetweenLength(1, 128));
11307
+ const Millis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
11308
+ /**
11309
+ * How long a turn's tool calls ran, summed, and its longest call, by tool: Claude Code's own
11310
+ * `duration_ms` for each, and, for Codex, whose hooks say none, from its `PreToolUse` to its
11311
+ * `PostToolUse` by `ref0 mcp`'s clock. Only calls that ended count: one with no end, as one a
11312
+ * `PreToolUse` answer denied or a Codex call cut short, isn't in it. `unknown`, never zero, when a
11313
+ * call that ended can't be timed, as one whose end says no time and whose start wasn't seen.
11314
+ */
11315
+ const ToolTime = Schema.Union([Schema.Struct({
11316
+ known: Schema.Literal("timed"),
11317
+ totalMs: Millis,
11318
+ /** None when the turn called no tool. */
11319
+ longest: Schema.optionalKey(Schema.Struct({
11320
+ tool: Name,
11321
+ ms: Millis
11322
+ }))
11323
+ }), Schema.Struct({ known: Schema.Literal("unknown") })]);
11032
11324
  /**
11033
- * A turn's facts but its tokens, which `TurnFacts` adds for whoever sends them and `turn.ended`
11034
- * carries only to those who may see them.
11325
+ * A compaction of the session's context during a turn: whether it was `auto` or `manual`, where
11326
+ * the harness says, as Claude Code does and Codex doesn't, and how long after ref0 started the
11327
+ * turn it happened, by the harness's own transcript, where both are known.
11328
+ */
11329
+ const Compaction = Schema.Struct({
11330
+ trigger: Schema.optionalKey(Schema.Literals(["auto", "manual"])),
11331
+ afterMs: Schema.optionalKey(Millis)
11332
+ });
11333
+ /**
11334
+ * A turn's context, read from the session's own transcript as its tokens are: `peak`, the most
11335
+ * one request of the session's own sent, its cached tokens included, none for a turn with no
11336
+ * request, and each compaction. `unknown`, with neither, where the transcript wasn't read whole.
11337
+ * A subagent's context is its own, and isn't in it.
11338
+ */
11339
+ const TurnContext = Schema.Union([Schema.Struct({
11340
+ known: Schema.Literal("read"),
11341
+ peak: Schema.optionalKey(TokenCount),
11342
+ compactions: Schema.Array(Compaction).check(Schema.isMaxLength(100))
11343
+ }), Schema.Struct({ known: Schema.Literal("unknown") })]);
11344
+ /**
11345
+ * One of the harness's own subagents that a turn ran, such as Claude Code's Task agents or
11346
+ * Codex's: its type, where the harness names one, and its own tokens, which the turn's tokens
11347
+ * already include, so the turn's less its subagents' are the session's own. Its tokens, and the
11348
+ * turn's, are `unknown` when the harness kept no transcript of it. Not a child session: ref0
11349
+ * knows it only from its `SubagentStop` and its transcript.
11350
+ */
11351
+ const TurnSubagent = Schema.Struct({
11352
+ type: Schema.optionalKey(Name),
11353
+ tokens: TurnTokens
11354
+ });
11355
+ /**
11356
+ * A turn's facts but its tokens and its subagents', which `TurnFacts` adds for whoever sends them
11357
+ * and `turn.ended` carries only to those who may see them.
11035
11358
  */
11036
11359
  const turnFactsFields = {
11037
11360
  /** The turn's model, where the harness said it: Codex in every hook. */
11038
- model: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 128))),
11361
+ model: Schema.optionalKey(Name),
11039
11362
  modelSource: ModelSource,
11040
11363
  /** Tool calls by tool name. */
11041
- 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")),
11364
+ tools: Schema.Record(Name, Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
11042
11365
  /** The files its edits touched, by repo and path, at most 50; `more` counts the rest. */
11043
11366
  addresses: Schema.Array(Schema.Struct({
11044
11367
  repo: RepoName,
11045
11368
  path: RepoPath
11046
11369
  })).check(Schema.isMaxLength(50)),
11047
- more: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
11370
+ more: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
11371
+ toolTime: ToolTime,
11372
+ context: TurnContext,
11373
+ /** How many subagents past the first 100 the turn ran. */
11374
+ moreSubagents: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
11048
11375
  };
11049
11376
  /**
11050
11377
  * What `ref0 mcp` saw of a turn, sent at its `Stop`s and when the next one starts, and again as
@@ -11056,16 +11383,23 @@ const turnFactsFields = {
11056
11383
  const TurnFacts = Schema.Struct({
11057
11384
  ...turnFactsFields,
11058
11385
  tokens: TurnTokens,
11059
- stoppedAfterMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
11386
+ /** The turn's subagents, in the order their transcripts were first read, at most 100. */
11387
+ subagents: Schema.Array(TurnSubagent).check(Schema.isMaxLength(100)),
11388
+ stoppedAfterMs: Schema.optionalKey(Millis)
11060
11389
  });
11061
11390
  /**
11062
- * A turn's facts as `turn.ended` shows them: its tokens only to an admin of the project's owner
11063
- * and to the session's accountable person. A thread's totals still go to every reader of it, in
11064
- * `ThreadUsage`, so on a thread with one person's sessions they are that person's usage.
11391
+ * A turn's facts as `turn.ended` shows them: its tokens and its subagents' only to an admin of
11392
+ * the project's owner and to the session's accountable person. A thread's totals still go to
11393
+ * every reader of it, in `ThreadUsage`, so on a thread with one person's sessions they are that
11394
+ * person's usage.
11065
11395
  */
11066
11396
  const EndedTurnFacts = Schema.Struct({
11067
11397
  ...turnFactsFields,
11068
- tokens: Schema.optionalKey(TurnTokens)
11398
+ tokens: Schema.optionalKey(TurnTokens),
11399
+ subagents: Schema.Array(Schema.Struct({
11400
+ type: TurnSubagent.fields.type,
11401
+ tokens: Schema.optionalKey(TurnTokens)
11402
+ })).check(Schema.isMaxLength(100))
11069
11403
  });
11070
11404
  /**
11071
11405
  * `POST /v1/sessions/{session}/turns`: a new turn, for the harness's own turn ID, so a retry, or
@@ -11693,151 +12027,6 @@ Schema.Struct({
11693
12027
  name: RepoName
11694
12028
  }))
11695
12029
  });
11696
- /** Where an agent's session asks for an identity token. */
11697
- const identityTokenPath = "/v1/me/identity-token";
11698
- /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
11699
- const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
11700
- /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
11701
- const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
11702
- /**
11703
- * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
11704
- * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
11705
- * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
11706
- */
11707
- const parsesAsWritten = (audience) => {
11708
- const parts = audiencePattern.exec(audience);
11709
- if (parts === null || !URL.canParse(audience)) return false;
11710
- const url = new URL(audience);
11711
- const port = parts[2] ?? "";
11712
- return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
11713
- };
11714
- /**
11715
- * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
11716
- * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
11717
- * case and any port too, so `https://verifier.example:443` is another audience than
11718
- * `https://verifier.example`. ref0 never fetches or resolves it.
11719
- */
11720
- 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"));
11721
- /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
11722
- const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
11723
- /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
11724
- 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"));
11725
- /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
11726
- const IssuedIdentityToken = Schema.Struct({
11727
- token: IdentityToken,
11728
- expiresAt: Schema.DateTimeUtcFromMillis
11729
- });
11730
- /**
11731
- * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
11732
- * token, naming its issuance in ref0's log without naming any row or secret.
11733
- */
11734
- 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"));
11735
- /**
11736
- * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
11737
- * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
11738
- * audience, different at every other, and carrying no time.
11739
- */
11740
- const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
11741
- /** A session, as one audience knows it, made as a person's is. */
11742
- const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
11743
- /** The accountable person, last in a token's chain: only their pairwise ID. */
11744
- const PersonActor = Schema.Struct({ sub: PairwisePersonId });
11745
- /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
11746
- const sessionActing = (act) => Schema.Struct({
11747
- sub: PairwiseSessionId,
11748
- act
11749
- });
11750
- const oneSession = sessionActing(PersonActor);
11751
- const twoSessions = sessionActing(oneSession);
11752
- const threeSessions = sessionActing(twoSessions);
11753
- /**
11754
- * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
11755
- * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
11756
- * allows the agent in `sub`, never an actor.
11757
- */
11758
- const IdentityActor = Schema.Union([
11759
- oneSession,
11760
- twoSessions,
11761
- threeSessions,
11762
- sessionActing(threeSessions)
11763
- ]);
11764
- /** A time in a token, in whole UTC epoch seconds. */
11765
- const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
11766
- Schema.Struct({
11767
- iss: ApiOrigin,
11768
- sub: AgentId,
11769
- aud: IdentityAudience,
11770
- iat: EpochSeconds,
11771
- exp: EpochSeconds,
11772
- jti: IdentityTokenId,
11773
- act: IdentityActor
11774
- }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
11775
- /**
11776
- * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
11777
- * never names two keys.
11778
- */
11779
- const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
11780
- Schema.Struct({
11781
- alg: Schema.Literal("ES256"),
11782
- kid: SigningKeyId,
11783
- typ: Schema.Literal("JWT")
11784
- });
11785
- /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
11786
- const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
11787
- /** One public key in the JWKS: exactly these fields, and never a private one. */
11788
- const PublicSigningKey = Schema.Struct({
11789
- kty: Schema.Literal("EC"),
11790
- crv: Schema.Literal("P-256"),
11791
- x: Coordinate,
11792
- y: Coordinate,
11793
- use: Schema.Literal("sig"),
11794
- alg: Schema.Literal("ES256"),
11795
- kid: SigningKeyId
11796
- });
11797
- Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
11798
- Schema.Struct({
11799
- issuer: ApiOrigin,
11800
- jwks_uri: Schema.String,
11801
- subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
11802
- id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
11803
- claims_supported: Schema.Tuple([
11804
- Schema.Literal("iss"),
11805
- Schema.Literal("sub"),
11806
- Schema.Literal("aud"),
11807
- Schema.Literal("exp"),
11808
- Schema.Literal("iat"),
11809
- Schema.Literal("jti"),
11810
- Schema.Literal("act")
11811
- ])
11812
- });
11813
- /**
11814
- * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
11815
- * `IdentityAudience` says. The refused text is never echoed.
11816
- */
11817
- var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
11818
- /**
11819
- * Less than a whole second is left before the session, a parent or the credential expires, or
11820
- * the token would expire before it's recorded: the next prompt renews or replaces the session.
11821
- */
11822
- var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
11823
- /**
11824
- * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
11825
- * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
11826
- * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
11827
- * received or used it. A failed append discards the token, so every token has its event.
11828
- */
11829
- const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
11830
- session: SessionId,
11831
- agent: AgentRef,
11832
- /** The person whose attachment authorized the session's chain, last in the token's `act`. */
11833
- accountable: PersonId,
11834
- jti: IdentityTokenId,
11835
- audience: IdentityAudience,
11836
- issuer: ApiOrigin,
11837
- keyId: SigningKeyId,
11838
- issuedAt: Schema.DateTimeUtcFromMillis,
11839
- expiresAt: Schema.DateTimeUtcFromMillis
11840
- }));
11841
12030
  Schema.Literals([
11842
12031
  "info/refs",
11843
12032
  "git-upload-pack",
@@ -11865,6 +12054,11 @@ Schema.Union([
11865
12054
  Schema.Struct({
11866
12055
  reader: Reader,
11867
12056
  name: RepoName,
12057
+ /**
12058
+ * For a fork's path, the scope it names: a project, by its ID or its name, or, with none, the
12059
+ * owner's own scope, which holds an organization's org threads.
12060
+ */
12061
+ scope: Schema.optionalKey(Schema.Struct({ project: Schema.optionalKey(ProjectRef) })),
11868
12062
  push: Schema.optionalKey(Schema.Struct({
11869
12063
  thread: ThreadId,
11870
12064
  session: SessionId
@@ -11878,7 +12072,9 @@ const GitTarget = Schema.Struct({
11878
12072
  * their sessions' forks, and the repo's `main`. Unset for one that reads the repo's project as
11879
12073
  * a whole.
11880
12074
  */
11881
- threads: Schema.optionalKey(Schema.Array(ThreadId))
12075
+ threads: Schema.optionalKey(Schema.Array(ThreadId)),
12076
+ /** For a fork's path, the project its scope resolved to: a project's, or the owner's own. */
12077
+ scope: Schema.optionalKey(ProjectId)
11882
12078
  });
11883
12079
  Schema.Union([GitTarget, RepoNotFound]);
11884
12080
  /** A repo whose baseline isn't readable yet: Git is told to try again in a few seconds. */
@@ -11889,28 +12085,6 @@ const CurrentBaseline = Schema.Struct({
11889
12085
  name: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/))
11890
12086
  });
11891
12087
  Schema.Union([CurrentBaseline, RepoNotReady]);
11892
- //#endregion
11893
- //#region ../contracts/src/fork-calls.ts
11894
- /**
11895
- * A session's fork of a repo: its own copy, made in the background from the repo's current
11896
- * baseline when the session first reaches the repo. Agents push only to their own session's
11897
- * forks. A fork isn't deleted yet: when it may be, and what's saved first, waits for the user.
11898
- */
11899
- /**
11900
- * Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
11901
- * project's visibility, which a session's routes read who sees the project by.
11902
- */
11903
- const SessionPlace = Schema.Struct({
11904
- owner: OwnerId,
11905
- project: ProjectId,
11906
- visibility: Visibility,
11907
- thread: ThreadId,
11908
- threadNumber: ThreadNumber,
11909
- session: SessionId,
11910
- number: SessionNumber,
11911
- /** Whether it's still going: not ended, and its lease not run out. */
11912
- live: Schema.Boolean
11913
- });
11914
12088
  Schema.Struct({ session: SessionId });
11915
12089
  Schema.Union([SessionPlace, SessionNotFound]);
11916
12090
  Schema.Struct({
@@ -15211,17 +15385,21 @@ const browserSession = HttpApiSecurity.apiKey({
15211
15385
  key: browserSessionCookie
15212
15386
  });
15213
15387
  /**
15214
- * Every endpoint's credential, checked before its handler runs: a bearer token, or a browser
15215
- * session's cookie. A request with both is refused before it gets here.
15388
+ * Every endpoint's credential, checked before its handler runs: a bearer token, a token bound to
15389
+ * a key with a proof that key signed (`Authorization: DPoP` and one `DPoP` header), or a browser
15390
+ * session's cookie. A request with a cookie and a token is refused before it gets here. A client
15391
+ * that signs proofs may fail before it sends anything, when its key store doesn't sign.
15216
15392
  */
15217
15393
  var Authorization = class extends HttpApiMiddleware.Service()("ref0/contracts/Authorization", {
15218
15394
  requiredForClient: true,
15219
15395
  security: {
15220
15396
  bearer: HttpApiSecurity.bearer,
15397
+ dpop: HttpApiSecurity.http({ scheme: "DPoP" }),
15221
15398
  browserSession
15222
15399
  },
15223
15400
  error: [
15224
15401
  Unauthenticated,
15402
+ InvalidDpopProof,
15225
15403
  SessionHasEnded,
15226
15404
  AuthorityUnavailable
15227
15405
  ]
@@ -15252,6 +15430,18 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
15252
15430
  * what to change. Any other part that fails its contract is answered with Effect's empty 400.
15253
15431
  */
15254
15432
  var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
15433
+ Context.Service()("ref0/contracts/ProvedKey");
15434
+ /**
15435
+ * A request with no credential, signed by a key it names, such as a machine's ask to connect: one
15436
+ * `DPoP` header, a proof with no `ath`, since there's no token, checked before the request is
15437
+ * read, which says the key that signed it; or no proof, which says none. Every client supplies
15438
+ * it, as it does `Authorization`, so its key store's failure, before anything is sent, is typed:
15439
+ * one with a key store signs, and every other sends no proof.
15440
+ */
15441
+ var KeyProof = class extends HttpApiMiddleware.Service()("ref0/contracts/KeyProof", {
15442
+ requiredForClient: true,
15443
+ error: InvalidDpopProof
15444
+ }) {};
15255
15445
  /**
15256
15446
  * An identity token's request whose audience its contract refuses, missing or not an HTTPS
15257
15447
  * origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
@@ -15367,8 +15557,9 @@ var AttachmentsGroup = class extends HttpApiGroup.make("attachments").add(HttpAp
15367
15557
  })).middleware(Authorization) {};
15368
15558
  /**
15369
15559
  * Connecting a machine, the machine's side: asking, with no credential, to connect to one owner's
15370
- * agent, whose terminal shows the code their person types; then collecting what came of it,
15371
- * confirming it saved the grant, or aborting, with its collecting secret.
15560
+ * agent, whose terminal shows the code their person types, signed by the machine key it names;
15561
+ * then collecting what came of it, confirming it saved the grant, or aborting, with its collecting
15562
+ * secret, which needs no proof: a grant it collects is bound to the key its ask named.
15372
15563
  */
15373
15564
  var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachments").add(HttpApiEndpoint.post("request", "/v1/attachments/requests", {
15374
15565
  headers: idempotency,
@@ -15378,9 +15569,10 @@ var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachment
15378
15569
  NotPermitted,
15379
15570
  CommandRefused,
15380
15571
  SlowDown,
15572
+ InvalidDpopProof,
15381
15573
  AuthorityUnavailable
15382
15574
  ]
15383
- }).middleware(MachineNameRefusal), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15575
+ }).middleware(MachineNameRefusal).middleware(KeyProof), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15384
15576
  params: { approval: ApprovalId },
15385
15577
  payload: Schema.Struct({ answerCode: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(64))) }),
15386
15578
  success: RequestState,
@@ -18854,6 +19046,8 @@ const HookArguments = Schema.Struct({
18854
19046
  tool_input: ToolValue,
18855
19047
  tool_response: ToolValue,
18856
19048
  error: Field,
19049
+ /** How long the tool ran, in milliseconds, at Claude Code's `PostToolUse` and its failure. */
19050
+ duration_ms: Field,
18857
19051
  /** A string from Claude Code, a boolean from Codex, which keeps a placeholder's JSON type. */
18858
19052
  stop_hook_active: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.String])),
18859
19053
  /** Codex names the turn's model in every hook but `SessionEnd`. */
@@ -18998,12 +19192,129 @@ const projectPage = (page, project) => page.replace("/$owner/$project", `/${proj
18998
19192
  * credential it sends, a bearer token or a browser session.
18999
19193
  */
19000
19194
  const ref0Client = (origin) => HttpApiClient.make(Api, origin === void 0 ? {} : { baseUrl: origin });
19195
+ /** No proof of a key for a request that asks for one: a client with no key store's. */
19196
+ const noKeyProof = HttpApiMiddleware.layerClient(KeyProof, ({ request, next }) => next(request));
19001
19197
  /** A bearer token: an agent's credential, sent with every request. */
19002
- const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19198
+ const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19003
19199
  /** No credential: a terminal asking its person, which sends nothing of theirs. */
19004
- const anonymousCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19200
+ const anonymousCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19005
19201
  FetchHttpClient.layer;
19006
19202
  //#endregion
19203
+ //#region ../sdk/src/client/proofs.ts
19204
+ /**
19205
+ * How far ref0's clock is ahead of this machine's, in milliseconds, as the `Date` of a refused
19206
+ * proof's answer said: provided once for a process, so every proof it signs after is dated by it.
19207
+ */
19208
+ var ProofClock = class ProofClock extends Context.Service()("ref0/sdk/ProofClock") {
19209
+ static layer = Layer.effect(ProofClock, Effect.map(Ref.make(0), (offset) => ProofClock.of({ offset })));
19210
+ };
19211
+ /** How far apart the two clocks may be before a refused proof is signed again by ref0's. */
19212
+ const clockSkewMillis = 5e3;
19213
+ const encodeHeader = Schema.encodeSync(Schema.fromJsonString(DpopProofHeader));
19214
+ const encodeClaims = Schema.encodeSync(Schema.fromJsonString(DpopProofClaims));
19215
+ const decodeProofId = Schema.decodeSync(DpopProofId);
19216
+ const utf8 = new TextEncoder();
19217
+ /** A fresh UUIDv4 from the platform's cryptographic source, a proof's `jti`. */
19218
+ const freshProofId = Effect.sync(() => {
19219
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
19220
+ bytes[6] = 64 | (bytes[6] ?? 0) & 15;
19221
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
19222
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
19223
+ return decodeProofId(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`);
19224
+ });
19225
+ const decodeThumbprint = Schema.decodeSync(KeyThumbprint);
19226
+ /**
19227
+ * RFC 7638's thumbprint of a P-256 public key: the SHA-256 of its required members, in order, as
19228
+ * JSON, in base64url. What a credential bound to the key names it by, as `cnf.jkt`.
19229
+ */
19230
+ 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}"}`))))));
19231
+ /** `ath`: the SHA-256 of the whole token, in base64url. */
19232
+ const tokenHashOf = (token) => Effect.promise(async () => Base64Url.encode(new Uint8Array(await crypto.subtle.digest("SHA-256", utf8.encode(token)))));
19233
+ /**
19234
+ * A proof for one attempt at a request: its method, and its URL as built, with no query or
19235
+ * fragment; dated by this machine's clock, moved by `offset`; with the token's hash, if it's sent
19236
+ * with one; signed by the store.
19237
+ */
19238
+ const proofFor = Effect.fnUntraced(function* (store, attempt, offset) {
19239
+ const now = (yield* Clock.currentTimeMillis) + offset;
19240
+ const header = encodeHeader({
19241
+ typ: "dpop+jwt",
19242
+ alg: "ES256",
19243
+ jwk: store.publicKey
19244
+ });
19245
+ const signs = {
19246
+ jti: yield* freshProofId,
19247
+ htm: attempt.method,
19248
+ htu: `${attempt.url.origin}${attempt.url.pathname}`,
19249
+ iat: Math.floor(now / 1e3)
19250
+ };
19251
+ if (attempt.token !== void 0) signs.ath = yield* tokenHashOf(attempt.token);
19252
+ const claims = encodeClaims(signs);
19253
+ const signed = `${Base64Url.encode(header)}.${Base64Url.encode(claims)}`;
19254
+ const signature = yield* store.sign(utf8.encode(signed));
19255
+ if (signature.byteLength !== 64) return yield* Effect.die("a key store answers r||s, 64 bytes");
19256
+ return `${signed}.${Base64Url.encode(signature)}`;
19257
+ });
19258
+ /**
19259
+ * How far ref0's clock was from this machine's, moved by `offset`, by a refused proof's answer:
19260
+ * none for any other answer, or one without a `Date`.
19261
+ */
19262
+ const skewOf = (response, local) => {
19263
+ const challenge = response.headers["www-authenticate"] ?? "";
19264
+ if (response.status !== 401 || !challenge.includes("DPoP error=\"invalid_dpop_proof\"")) return Option.none();
19265
+ return Option.map(DateTime.make(response.headers["date"] ?? ""), (date) => DateTime.toEpochMillis(date) - local);
19266
+ };
19267
+ /**
19268
+ * Sends one request, with its credential if it has one, signing each attempt afresh: once, and
19269
+ * once more, by ref0's clock, after a refused proof whose answer is dated more than five seconds
19270
+ * from this machine's. The process keeps the offset for every proof after.
19271
+ */
19272
+ const signedAttempts = (credential, offsetRef) => ({ request, next }) => Effect.gen(function* () {
19273
+ const token = credential.token === void 0 ? void 0 : Redacted.value(credential.token);
19274
+ const attempt = {
19275
+ method: request.method,
19276
+ url: new URL(`${credential.origin}${request.url}`),
19277
+ token
19278
+ };
19279
+ const redacted = [...yield* Effect.service(Headers.CurrentRedactedNames), "dpop"];
19280
+ const send = (offset) => proofFor(credential.store, attempt, offset).pipe(Effect.map((proof) => HttpClientRequest.setHeaders(request, token === void 0 ? { dpop: proof } : {
19281
+ authorization: `DPoP ${token}`,
19282
+ dpop: proof
19283
+ })), Effect.flatMap(next), Effect.provideService(Headers.CurrentRedactedNames, redacted));
19284
+ const offset = yield* Ref.get(offsetRef);
19285
+ const answer = yield* send(offset);
19286
+ const skew = skewOf(answer, (yield* Clock.currentTimeMillis) + offset);
19287
+ if (Option.isNone(skew) || Math.abs(skew.value) <= clockSkewMillis) return answer;
19288
+ yield* Effect.ignore(answer.text);
19289
+ yield* Ref.set(offsetRef, offset + skew.value);
19290
+ return yield* send(offset + skew.value);
19291
+ });
19292
+ /**
19293
+ * A token bound to a key, sent as `Authorization: DPoP` with a proof its key store signs afresh
19294
+ * for each attempt. Signing in's own endpoints take no token.
19295
+ */
19296
+ 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);
19297
+ /**
19298
+ * ref0's `/v1` as a typed client at a bound credential's origin, sending its token with a proof
19299
+ * its key store signs for each attempt. The client is made at the origin its proofs name, so
19300
+ * neither the token nor a proof is sent anywhere else. It needs the process's `ProofClock`. A
19301
+ * request's trace redacts its proof, but an error it fails with still holds the request: a
19302
+ * program that logs errors adds `dpop` to `Headers.CurrentRedactedNames` at its root.
19303
+ */
19304
+ const boundClient = (credential) => ref0Client(credential.origin).pipe(Effect.provide(dpopCredential(credential)));
19305
+ /**
19306
+ * A request with no credential, signed by the key it names, such as a machine's ask to connect:
19307
+ * a proof its key store signs for each attempt, with no `ath`, since there's no token. Any other
19308
+ * request is sent with nothing added.
19309
+ */
19310
+ 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)));
19311
+ /**
19312
+ * ref0's `/v1` as a typed client at a key's origin, with no credential, signing each request that
19313
+ * asks for a proof, such as a machine's ask to connect, with the key its store holds. It needs the
19314
+ * process's `ProofClock`, and its errors still hold their requests, as `boundClient`'s do.
19315
+ */
19316
+ const provingClient = (signer) => ref0Client(signer.origin).pipe(Effect.provide(keyProof(signer)));
19317
+ //#endregion
19007
19318
  //#region ../sdk/src/client/request-id.ts
19008
19319
  const decodeRequestId$1 = Schema.decodeUnknownEffect(RequestId);
19009
19320
  /**
@@ -19371,7 +19682,7 @@ const refusedBy = (status, refusal, change) => new CallFailed({
19371
19682
  * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
19372
19683
  * the CLI made. A change it answered may have run.
19373
19684
  */
19374
- const unreadable = (endpoint, change) => new CallFailed({
19685
+ const unreadable$1 = (endpoint, change) => new CallFailed({
19375
19686
  status: 0,
19376
19687
  kind: "Unexpected",
19377
19688
  nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
@@ -19420,7 +19731,7 @@ const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, e
19420
19731
  const state = answered.value;
19421
19732
  heardAt = now;
19422
19733
  if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
19423
- onNone: () => Effect.fail(unreadable(endpoint, change)),
19734
+ onNone: () => Effect.fail(unreadable$1(endpoint, change)),
19424
19735
  onSome: Result.match({
19425
19736
  onSuccess: () => Effect.succeed(state.result),
19426
19737
  onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
@@ -19537,7 +19848,7 @@ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19537
19848
  onNone: () => asPerson(request.command, change, command.endpoint),
19538
19849
  onSome: (bearer) => asSession(bearer, request, command.endpoint)
19539
19850
  });
19540
- if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19851
+ if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable$1(request.command.endpoint, change);
19541
19852
  return answer;
19542
19853
  });
19543
19854
  return Ref0.of({ call });
@@ -19552,7 +19863,7 @@ const callEndpoint = (command) => Effect.gen(function* () {
19552
19863
  return {
19553
19864
  json,
19554
19865
  value: yield* Option.match(successOf(command.endpoint, json), {
19555
- onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19866
+ onNone: () => Effect.fail(unreadable$1(command.endpoint, false)),
19556
19867
  onSome: Effect.succeed
19557
19868
  })
19558
19869
  };
@@ -21337,18 +21648,182 @@ var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
21337
21648
  static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
21338
21649
  };
21339
21650
  //#endregion
21651
+ //#region src/machine-key.ts
21652
+ /**
21653
+ * This machine's key, which signs a proof for its ask to connect and for each request a host
21654
+ * grant makes: one ES256 key for each OS account and origin, in the origin's keychain service
21655
+ * under account `machine-key`, as `{ store: 'software', jwk }`. `ref0 connect` makes it, under its
21656
+ * lock, only when the keychain says it holds none, and nothing else makes one. A process reads it
21657
+ * once and keeps it, since each read is a `security` or `secret-tool` process. Its private half
21658
+ * goes to the keychain on stdin and into a Web Crypto key that can't be exported, and nowhere
21659
+ * else: no argument, environment, config, error or log.
21660
+ */
21661
+ /** The keychain account the key is kept under. */
21662
+ const account = "machine-key";
21663
+ /** How long reading and importing the key may take for a request, and signing with it. */
21664
+ const deadline = Duration.seconds(3);
21665
+ /**
21666
+ * How long `ref0 connect` may wait on the keychain to read, make and keep the key, as its probe
21667
+ * may: a prompt to unlock the keychain can take that long.
21668
+ */
21669
+ const connectDeadline = Duration.minutes(1);
21670
+ /** A P-256 coordinate or private scalar: 32 bytes, in base64url. */
21671
+ const Scalar = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/));
21672
+ /** A P-256 private key as a JWK: its public point, and its private scalar `d`. */
21673
+ const PrivateJwk = Schema.Struct({
21674
+ kty: Schema.Literal("EC"),
21675
+ crv: Schema.Literal("P-256"),
21676
+ x: Scalar,
21677
+ y: Scalar,
21678
+ d: Scalar
21679
+ });
21680
+ /** The keychain's record: which store keeps the key, and the key. A hardware store keeps its own. */
21681
+ const KeptKey = Schema.Struct({
21682
+ store: Schema.Literal("software"),
21683
+ jwk: PrivateJwk
21684
+ });
21685
+ const decodeKept$3 = Schema.decodeUnknownOption(Schema.fromJsonString(KeptKey));
21686
+ const encodeKept$3 = Schema.encodeSync(Schema.fromJsonString(KeptKey));
21687
+ const decodePrivateJwk = Schema.decodeUnknownEffect(PrivateJwk);
21688
+ const decodePublicJwk = Schema.decodeSync(PublicJwk);
21689
+ const ecdsa = {
21690
+ name: "ECDSA",
21691
+ namedCurve: "P-256"
21692
+ };
21693
+ const es256 = {
21694
+ name: "ECDSA",
21695
+ hash: "SHA-256"
21696
+ };
21697
+ /** What a key signs to prove it signs for its public half. */
21698
+ const proved = new TextEncoder().encode("ref0 machine key");
21699
+ const timedOut = new KeychainError({ nextStep: "The keychain didn’t answer in time. Check that it’s unlocked, and try again." });
21700
+ const unreadable = 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." });
21701
+ const notKept = new KeychainError({ nextStep: "The keychain didn’t keep this machine’s key. Check that it’s unlocked, then run ref0 connect again." });
21702
+ 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." });
21703
+ const didNotSign = new KeyStoreUnavailable({ nextStep: "This machine’s key didn’t sign in time. Try again." });
21704
+ /** A fresh key's private JWK, with only the members the record keeps. */
21705
+ const freshKey = Effect.promise(async () => {
21706
+ const pair = await crypto.subtle.generateKey(ecdsa, true, ["sign", "verify"]);
21707
+ return await crypto.subtle.exportKey("jwk", pair.privateKey);
21708
+ }).pipe(Effect.flatMap(({ kty, crv, x, y, d }) => decodePrivateJwk({
21709
+ kty,
21710
+ crv,
21711
+ x,
21712
+ y,
21713
+ d
21714
+ })), Effect.orDie);
21715
+ /**
21716
+ * A key store over a private JWK: imported once, as a key that can't be exported, and proved to
21717
+ * sign for its public half, or it's no key to bind anything to.
21718
+ */
21719
+ const softwareStore = Effect.fnUntraced(function* (jwk) {
21720
+ const publicKey = decodePublicJwk({
21721
+ kty: jwk.kty,
21722
+ crv: jwk.crv,
21723
+ x: jwk.x,
21724
+ y: jwk.y
21725
+ });
21726
+ const signs = yield* Effect.tryPromise({
21727
+ try: async () => {
21728
+ const [signing, verifying] = await Promise.all([crypto.subtle.importKey("jwk", jwk, ecdsa, false, ["sign"]), crypto.subtle.importKey("jwk", publicKey, ecdsa, false, ["verify"])]);
21729
+ const signature = await crypto.subtle.sign(es256, signing, proved);
21730
+ return await crypto.subtle.verify(es256, verifying, signature, proved) ? signing : void 0;
21731
+ },
21732
+ catch: () => unreadable
21733
+ });
21734
+ if (signs === void 0) return yield* unreadable;
21735
+ const sign = (bytes) => Effect.tryPromise({
21736
+ try: async () => new Uint8Array(await crypto.subtle.sign(es256, signs, new Uint8Array(bytes))),
21737
+ catch: () => didNotSign
21738
+ }).pipe(Effect.timeoutOrElse({
21739
+ duration: deadline,
21740
+ orElse: () => Effect.fail(didNotSign)
21741
+ }));
21742
+ return {
21743
+ store: {
21744
+ publicKey,
21745
+ sign
21746
+ },
21747
+ binding: { jkt: yield* keyThumbprint(publicKey) }
21748
+ };
21749
+ });
21750
+ /**
21751
+ * This machine's key, read once for the process. `supported` refuses a platform with no keychain
21752
+ * that keeps it, such as Windows, before `ref0 connect` asks the keychain anything. `ensure` is
21753
+ * `ref0 connect`'s: the key the keychain holds, or one made and kept now if it says it holds none,
21754
+ * under the connect lock, once the keychain is probed. `boundTo` is for a kept
21755
+ * credential bound to a key: the key it names, read again once if the one held is another, such
21756
+ * as one replaced since, or `InvalidDpopProof`, without sending anything, when the keychain has
21757
+ * none or another. Neither makes a key from an item it can't read.
21758
+ */
21759
+ var MachineKey = class MachineKey extends Context.Service()("ref0/cli/MachineKey") {
21760
+ /** On the platform Node names, such as `win32`, where no keychain keeps the key yet. */
21761
+ static layerOn = (platform) => Layer.effect(MachineKey, Effect.gen(function* () {
21762
+ const keychain = yield* Keychain;
21763
+ const held = yield* Ref.make(Option.none());
21764
+ const reading = yield* Semaphore.make(1);
21765
+ const read = Effect.gen(function* () {
21766
+ const kept = yield* keychain.get(account);
21767
+ if (Option.isNone(kept)) return Option.none();
21768
+ const record = decodeKept$3(Redacted.value(kept.value));
21769
+ if (Option.isNone(record)) return yield* unreadable;
21770
+ const key = yield* softwareStore(record.value.jwk);
21771
+ yield* Ref.set(held, Option.some(key));
21772
+ return Option.some(key);
21773
+ });
21774
+ const made = Effect.gen(function* () {
21775
+ const jwk = yield* freshKey;
21776
+ yield* keychain.set(account, Redacted.make(encodeKept$3({
21777
+ store: "software",
21778
+ jwk
21779
+ })));
21780
+ const back = yield* read;
21781
+ const same = Option.exists(back, (key) => key.store.publicKey.x === jwk.x && key.store.publicKey.y === jwk.y);
21782
+ if (Option.isNone(back) || !same) return yield* notKept;
21783
+ return back.value;
21784
+ });
21785
+ const supported = platform === "win32" ? Effect.fail(windows) : Effect.void;
21786
+ const ensure = Effect.gen(function* () {
21787
+ const kept = yield* read;
21788
+ return Option.isSome(kept) ? kept.value : yield* made;
21789
+ }).pipe(Effect.timeoutOrElse({
21790
+ duration: connectDeadline,
21791
+ orElse: () => Effect.fail(timedOut)
21792
+ }), reading.withPermits(1));
21793
+ const boundTo = (binding) => Effect.gen(function* () {
21794
+ const now = yield* Ref.get(held);
21795
+ if (Option.isSome(now) && now.value.binding.jkt === binding.jkt) return now.value.store;
21796
+ const kept = yield* read.pipe(Effect.timeoutOrElse({
21797
+ duration: deadline,
21798
+ orElse: () => Effect.fail(timedOut)
21799
+ }));
21800
+ if (Option.isSome(kept) && kept.value.binding.jkt === binding.jkt) return kept.value.store;
21801
+ return yield* invalidDpopProof;
21802
+ }).pipe(reading.withPermits(1));
21803
+ return MachineKey.of({
21804
+ supported,
21805
+ ensure,
21806
+ boundTo
21807
+ });
21808
+ }));
21809
+ static layer = MachineKey.layerOn(process.platform);
21810
+ };
21811
+ //#endregion
21340
21812
  //#region src/host-grant.ts
21341
21813
  /**
21342
21814
  * All lasting authority for one config folder, stored as one keychain value: the agent the folder
21343
- * acts as, by its owner, handle and agent ID, and its token. The ID is kept in the same write as
21344
- * the token, so a folder with a grant always knows which agent its commits name.
21815
+ * acts as, by its owner, handle and agent ID, its token, and the key it's bound to, if it is. The
21816
+ * ID is kept in the same write as the token, so a folder with a grant always knows which agent its
21817
+ * commits name.
21345
21818
  */
21346
21819
  const KeptHostGrant = Schema.Struct({
21347
21820
  harness: ConnectHarness,
21348
21821
  owner: OwnerName,
21349
21822
  agent: AgentHandle,
21350
21823
  agentId: AgentId,
21351
- token: Schema.RedactedFromValue(Schema.String)
21824
+ token: Schema.RedactedFromValue(Schema.String),
21825
+ /** This machine's key, by its thumbprint, when ref0 bound the grant to it: none from before. */
21826
+ cnf: Schema.optionalKey(KeyBinding)
21352
21827
  });
21353
21828
  const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
21354
21829
  const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
@@ -21392,6 +21867,24 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21392
21867
  }
21393
21868
  return Option.none();
21394
21869
  });
21870
+ const decodeProofOrigin$1 = Schema.decodeUnknownEffect(ProofOrigin);
21871
+ /**
21872
+ * ref0's `/v1` as a typed client acting with a kept host grant: with a proof this machine's key
21873
+ * signs for each request when the grant is bound to it, or as a bearer token when it's a grant
21874
+ * from before binding. A grant bound to a key the keychain no longer holds is refused here, with
21875
+ * `InvalidDpopProof`, and nothing is sent.
21876
+ */
21877
+ const grantClient = Effect.fnUntraced(function* (grant) {
21878
+ const config = yield* Ref0Config;
21879
+ if (grant.cnf === void 0) return yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.token)));
21880
+ const store = yield* (yield* MachineKey).boundTo(grant.cnf);
21881
+ const origin = yield* Effect.orDie(decodeProofOrigin$1(config.api));
21882
+ return yield* boundClient({
21883
+ origin,
21884
+ store,
21885
+ token: grant.token
21886
+ });
21887
+ });
21395
21888
  //#endregion
21396
21889
  //#region src/version.ts
21397
21890
  /**
@@ -21400,7 +21893,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21400
21893
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21401
21894
  * and only as a default export.
21402
21895
  */
21403
- const cliVersion = "0.12.0";
21896
+ const cliVersion = "0.13.0";
21404
21897
  //#endregion
21405
21898
  //#region src/harness/hook-protocol.ts
21406
21899
  /**
@@ -21650,6 +22143,7 @@ const yesOrNoQuestions = Effect.gen(function* () {
21650
22143
  const isAnswerCode = Schema.is(TypedApprovalCode);
21651
22144
  const decodeTokenId$1 = Schema.decodeUnknownOption(TokenId);
21652
22145
  const decodeSecretHash$1 = Schema.decodeUnknownEffect(SecretHash);
22146
+ const decodeProofOrigin = Schema.decodeUnknownEffect(ProofOrigin);
21653
22147
  /** How often the machine asks whether its person has answered. */
21654
22148
  const pollMillis = 2e3;
21655
22149
  /** How long one request may take before the machine gives up on it, and asks again. */
@@ -21702,12 +22196,14 @@ const endedWords$1 = (state) => {
21702
22196
  /** The grant a kept record holds, by its ID. */
21703
22197
  const grantOf = (token) => Option.flatMap(parseToken(Redacted.value(token)), (parts) => decodeTokenId$1(parts.token));
21704
22198
  /**
21705
- * Asks with no owner for every folder given, each with a grant secret of its own, shows the
21706
- * link, and takes the answer from the person's own page. Then it keeps each folder ref0
21707
- * connected, confirms exactly the grants it kept, and forgets 0.5.0's grants they replace.
22199
+ * Asks with no owner for every folder given, each with a grant secret of its own, signed by this
22200
+ * machine's key, which ref0 binds every grant to, shows the link, and takes the answer from the
22201
+ * person's own page. Then it keeps each folder ref0 connected, with the key it's bound to,
22202
+ * confirms exactly the grants it kept, and forgets 0.5.0's grants they replace.
21708
22203
  */
21709
- const requestAttachment = Effect.fnUntraced(function* (asked) {
22204
+ const requestAttachment = Effect.fnUntraced(function* (asked, key) {
21710
22205
  const config = yield* Ref0Config;
22206
+ 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.`)));
21711
22207
  const machine = yield* Machine;
21712
22208
  const hostGrant = yield* HostGrant;
21713
22209
  const oldGrants = yield* OldHostGrants;
@@ -21720,7 +22216,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21720
22216
  const opener = yield* browserOpener;
21721
22217
  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)));
21722
22218
  const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
21723
- const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
22219
+ const client = yield* provingClient({
22220
+ origin,
22221
+ store: key.store
22222
+ }).pipe(Effect.provideService(HttpClient.HttpClient, http));
21724
22223
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
21725
22224
  const requestId = yield* newRequestId;
21726
22225
  const asking = yield* Effect.forEach(asked, Effect.fnUntraced(function* (folder) {
@@ -21747,6 +22246,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21747
22246
  const payload = {
21748
22247
  machine: machineId,
21749
22248
  machineName: machineNameOf(machine.hostName),
22249
+ publicKey: key.store.publicKey,
21750
22250
  folders,
21751
22251
  collectHash: yield* Effect.orDie(decodeSecretHash$1(collecting.hash)),
21752
22252
  linkHash: yield* Effect.orDie(decodeSecretHash$1(link.hash))
@@ -21762,6 +22262,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21762
22262
  NotPermitted: said,
21763
22263
  CommandRefused: said,
21764
22264
  SlowDown: said,
22265
+ KeyStoreUnavailable: said,
22266
+ 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.`),
21765
22267
  AuthorityUnavailable: unanswered,
21766
22268
  HttpClientError: unanswered,
21767
22269
  SchemaError: unanswered
@@ -21785,10 +22287,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21785
22287
  const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
21786
22288
  const collector = yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http));
21787
22289
  const params = { approval: started.approval };
21788
- const collect = Effect.flatMap(Ref.get(pasted), (answerCode) => collector.machineAttachments.collect({
22290
+ const collect = (answerCode) => collector.machineAttachments.collect({
21789
22291
  params,
21790
22292
  payload: answerCode === void 0 ? {} : { answerCode }
21791
- })).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
22293
+ }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
21792
22294
  duration: requestTimeout,
21793
22295
  orElse: () => Effect.succeedNone
21794
22296
  }), Effect.catchTags({
@@ -21798,13 +22300,19 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21798
22300
  HttpClientError: () => Effect.succeedNone,
21799
22301
  SchemaError: () => Effect.succeedNone
21800
22302
  }));
22303
+ const lapsing = asking.some((folder) => Option.isSome(folder.replaces)) ? "Its new grants expire unconfirmed, and each folder’s connection from before still works." : "Its new grants expire unconfirmed.";
21801
22304
  const giveUpAt = DateTime.toEpochMillis(started.expiresAt) + giveUpAfterMillis;
22305
+ let confirmBy;
21802
22306
  const answered = Effect.gen(function* () {
21803
22307
  while (true) {
21804
- const state = yield* collect;
21805
- if (Option.isSome(state) && isEnded(state.value)) return state.value;
21806
- if ((yield* Clock.currentTimeMillis) > giveUpAt) {
22308
+ const sent = yield* Ref.get(pasted);
22309
+ const state = Option.getOrUndefined(yield* collect(sent));
22310
+ if (state !== void 0 && isEnded(state)) return state;
22311
+ if (state?.confirmBy !== void 0) confirmBy = DateTime.toEpochMillis(state.confirmBy);
22312
+ if ((yield* Ref.get(pasted)) !== sent) continue;
22313
+ if ((yield* Clock.currentTimeMillis) > (confirmBy ?? giveUpAt)) {
21807
22314
  yield* endWait;
22315
+ if (confirmBy !== void 0 && sent === void 0) return yield* sayFailed(`It was approved, but no code was pasted in time, so this machine kept nothing. ${lapsing} To connect this machine, run: ${again$1}`);
21808
22316
  return yield* sayFailed(`ref0 hasn’t said how it ended, so this machine kept nothing. If it was approved, ref0 revokes its grants 10 minutes after it connected. To connect this machine, run: ${again$1}`);
21809
22317
  }
21810
22318
  yield* Effect.sleep(pollMillis);
@@ -21827,7 +22335,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21827
22335
  owner: connection.owner,
21828
22336
  agent: connection.handle,
21829
22337
  agentId: connection.agent.id,
21830
- token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
22338
+ token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret)),
22339
+ cnf: key.binding
21831
22340
  }, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21832
22341
  const finish = Effect.fnUntraced(function* (state) {
21833
22342
  if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
@@ -21949,7 +22458,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21949
22458
  times: 2
21950
22459
  }), Effect.catchTag("Unanswered", () => sayFailed("Couldn’t reach ref0 to abort it. If it’s approved anyway, ref0 revokes its grants 10 minutes after they connect, since this machine never kept them.")));
21951
22460
  if (!isEnded(state)) {
21952
- if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", asking.some((folder) => Option.isSome(folder.replaces)) ? "Its new grants expire unconfirmed, and each folder’s connection from before still works." : "Its new grants expire unconfirmed."]]);
22461
+ if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", lapsing]]);
21953
22462
  yield* printBlocks([["It was already answered, so this machine waits to hear how."]]);
21954
22463
  }
21955
22464
  const connected = yield* finish(isEnded(state) ? state : yield* answered);
@@ -24628,14 +25137,17 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
24628
25137
  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.");
24629
25138
  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.");
24630
25139
  yield* (yield* ConnectLock).hold;
25140
+ const machineKey = yield* MachineKey;
25141
+ yield* machineKey.supported;
24631
25142
  yield* (yield* Keychain).probe;
25143
+ const key = yield* machineKey.ensure;
24632
25144
  const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
24633
25145
  const found = yield* findConfigFolders;
24634
25146
  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.`);
24635
25147
  const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
24636
25148
  if (chosen.length === 0) return yield* sayFailed(`No config folder was ticked, so nothing was asked. To connect one, run ${again} again.`);
24637
25149
  yield* Console.error("");
24638
- const connected = yield* requestAttachment(chosen);
25150
+ const connected = yield* requestAttachment(chosen, key);
24639
25151
  const installed = yield* installInto(connected.kept, { asJson });
24640
25152
  const others = yield* otherOwners(connected);
24641
25153
  yield* printAnswer(answerOf(connected, installed, others), asJson, reportOf(connected, installed, others, home));
@@ -25201,9 +25713,9 @@ const signalExits = {
25201
25713
  };
25202
25714
  const decodeSetup = Schema.decodeUnknownOption(Schema.Array(Schema.String));
25203
25715
  /** A duration's milliseconds, as a check writes it, `'30 seconds'`, or a number of them. */
25204
- const millisOf = (duration) => Option.map(Duration.fromInput(duration), (read) => Math.ceil(Duration.toMillis(read)));
25716
+ const millisOf$1 = (duration) => Option.map(Duration.fromInput(duration), (read) => Math.ceil(Duration.toMillis(read)));
25205
25717
  /** A duration a command was given, in milliseconds, or why it isn't one. */
25206
- const givenMillis = (duration) => Option.match(millisOf(duration), {
25718
+ const givenMillis = (duration) => Option.match(millisOf$1(duration), {
25207
25719
  onNone: () => Effect.fail(new LocalSandboxFailed({ why: `Not a duration: ${duration}` })),
25208
25720
  onSome: Effect.succeed
25209
25721
  });
@@ -25546,7 +26058,7 @@ const defaultRetries = {
25546
26058
  };
25547
26059
  const defaultTimeout = 6e5;
25548
26060
  /** A duration as a step's config writes it, in milliseconds, or the run's code's mistake. */
25549
- const stepMillis = (duration) => Option.match(millisOf(duration), {
26061
+ const stepMillis = (duration) => Option.match(millisOf$1(duration), {
25550
26062
  onNone: () => Effect.die(/* @__PURE__ */ new Error(`Not a duration: ${duration}`)),
25551
26063
  onSome: Effect.succeed
25552
26064
  });
@@ -26866,7 +27378,7 @@ const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
26866
27378
  * or the thread below, set apart.
26867
27379
  */
26868
27380
  const baseNamed = (on, main = "main") => on === "main" ? main : `#${on.thread}, which this thread is stacked on,`;
26869
- const decodeWritten$1 = Schema.decodeUnknownOption(WrittenRepo);
27381
+ const decodeWritten$2 = Schema.decodeUnknownOption(WrittenRepo);
26870
27382
  const decodeCommit$1 = Schema.decodeUnknownOption(CommitOid);
26871
27383
  const refused$1 = (why) => ({
26872
27384
  kind: "refused",
@@ -26900,7 +27412,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
26900
27412
  stdout: "inherit",
26901
27413
  stderr: "inherit"
26902
27414
  })).pipe(Effect.map(Number), Effect.catchCause(() => Effect.succeed(-1)));
26903
- const written = decodeWritten$1((yield* read([
27415
+ const written = decodeWritten$2((yield* read([
26904
27416
  "config",
26905
27417
  "--get",
26906
27418
  "ref0.repo"
@@ -30066,7 +30578,7 @@ const watchCommands = [Command.make("watch", {
30066
30578
  owner: ownerName,
30067
30579
  agent: flags.agent
30068
30580
  });
30069
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)));
30581
+ const client = yield* grantClient(grant.value);
30070
30582
  const watching = {
30071
30583
  project: flags.project,
30072
30584
  parent: Option.getOrUndefined(flags.parent),
@@ -30095,7 +30607,7 @@ const watchCommands = [Command.make("watch", {
30095
30607
  parent: number
30096
30608
  })
30097
30609
  })
30098
- }).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}` })))));
30610
+ }).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}` })))));
30099
30611
  return yield* Effect.scoped(Effect.gen(function* () {
30100
30612
  const state = yield* openWatchState(folder, {
30101
30613
  project: flags.project,
@@ -30115,7 +30627,8 @@ const watchCommands = [Command.make("watch", {
30115
30627
  }).pipe(Effect.catchTags({
30116
30628
  WatchStopped: (stopped) => sayFailed(stopped.nextStep),
30117
30629
  WatchStateError: (failed) => sayFailed(failed.nextStep),
30118
- KeychainError: (failed) => sayFailed(failed.nextStep)
30630
+ KeychainError: (failed) => sayFailed(failed.nextStep),
30631
+ InvalidDpopProof: (refused) => sayFailed(refused.nextStep)
30119
30632
  }))).pipe(Command.withShortDescription("Run a command for each thread started in a project"), Command.withDescription([
30120
30633
  "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.",
30121
30634
  "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.",
@@ -30652,7 +31165,7 @@ function editedPaths(hook) {
30652
31165
  /** The most bytes of a file read around an edit; a larger one's edits aren't attributed. */
30653
31166
  const fileBytes = 2e6;
30654
31167
  /** The most edit calls waiting for their `PostToolUse`; a harness that never sends one leaks none. */
30655
- const openCalls = 64;
31168
+ const openCalls$1 = 64;
30656
31169
  /** The most line hashes a worktree keeps pending; the oldest edits go first. */
30657
31170
  const pendingLines = 2e4;
30658
31171
  /** A line as it's kept pending: 128 bits of its SHA-256, never its text. */
@@ -31021,7 +31534,7 @@ var Attribution = class Attribution extends Context.Service()("ref0/cli/Attribut
31021
31534
  turn,
31022
31535
  files
31023
31536
  });
31024
- for (const key of calls.keys()) if (calls.size > openCalls) calls.delete(key);
31537
+ for (const key of calls.keys()) if (calls.size > openCalls$1) calls.delete(key);
31025
31538
  return {
31026
31539
  ...latest,
31027
31540
  calls
@@ -31269,7 +31782,7 @@ const optionsWithValues = /* @__PURE__ */ new Set([
31269
31782
  "--git-dir",
31270
31783
  "--work-tree"
31271
31784
  ]);
31272
- const decodeWritten = Schema.decodeUnknownOption(WrittenRepo);
31785
+ const decodeWritten$1 = Schema.decodeUnknownOption(WrittenRepo);
31273
31786
  const hunkHeader = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
31274
31787
  /** The command a Git run's arguments name, past Git's own options. */
31275
31788
  const commandOf = (args) => {
@@ -31617,7 +32130,7 @@ const checkBase = Effect.fnUntraced(function* (folder, base, delivered, within)
31617
32130
  "--get",
31618
32131
  "ref0.repo"
31619
32132
  ]);
31620
- const repo = decodeWritten(named.out.trim());
32133
+ const repo = decodeWritten$1(named.out.trim());
31621
32134
  if (Option.isNone(repo)) return yield* failed("this checkout has no ref0.repo to load history for");
31622
32135
  yield* loadSyncHistory(store, repo.value).pipe(Effect.mapError((error) => failed(error.why)));
31623
32136
  yield* fs.writeFileString(loaded, "");
@@ -32375,6 +32888,321 @@ var HarnessTasks = class HarnessTasks extends Context.Service()("ref0/cli/Harnes
32375
32888
  }));
32376
32889
  };
32377
32890
  //#endregion
32891
+ //#region src/harness/turn-facts.ts
32892
+ const Written = Schema.Struct({ timestamp: Schema.String });
32893
+ const decodeWritten = Schema.decodeUnknownOption(Schema.fromJsonString(Written));
32894
+ /** When a transcript's line says it was written, by the harness's clock, if it says. */
32895
+ const writtenAt = (line) => {
32896
+ const at = Option.match(decodeWritten(line), {
32897
+ onNone: () => NaN,
32898
+ onSome: (written) => Date.parse(written.timestamp)
32899
+ });
32900
+ return Number.isFinite(at) ? at : void 0;
32901
+ };
32902
+ /** The most tool names a turn's record counts. */
32903
+ const toolNames = 100;
32904
+ /** The most replies a turn's tally remembers; a turn with more has its tokens unknown. */
32905
+ const turnReplies = 1e4;
32906
+ /**
32907
+ * The most calls a turn remembers as begun and not ended, its newest kept: a call that never
32908
+ * ends, as one a `PreToolUse` answer denied, is never counted.
32909
+ */
32910
+ const openCalls = 100;
32911
+ const nothingRead = {
32912
+ known: "waiting",
32913
+ models: /* @__PURE__ */ new Map(),
32914
+ replies: /* @__PURE__ */ new Map()
32915
+ };
32916
+ const nothingReadOf = {
32917
+ tokens: nothingRead,
32918
+ context: {
32919
+ known: "waiting",
32920
+ peak: void 0,
32921
+ compactions: []
32922
+ },
32923
+ subagents: /* @__PURE__ */ new Map(),
32924
+ moreSubagents: 0
32925
+ };
32926
+ const nothingCalled = {
32927
+ known: true,
32928
+ totalMs: 0,
32929
+ longest: void 0,
32930
+ open: /* @__PURE__ */ new Map()
32931
+ };
32932
+ const nothingSeen = {
32933
+ ...nothingReadOf,
32934
+ tools: /* @__PURE__ */ new Map(),
32935
+ calls: nothingCalled,
32936
+ addresses: [],
32937
+ more: 0,
32938
+ model: void 0,
32939
+ startedAt: void 0,
32940
+ stoppedAt: void 0
32941
+ };
32942
+ /**
32943
+ * A call's `PreToolUse` or its end seen at `at`: its time Claude Code's `duration_ms`, or, where
32944
+ * the harness says none, as Codex, from its `PreToolUse` to its end. One whose time can't be
32945
+ * told makes the turn's unknown, as does a tool name ref0 refuses.
32946
+ */
32947
+ function timeCall(calls, hook, at) {
32948
+ const name = hook.tool?.name;
32949
+ const callId = hook.tool?.callId;
32950
+ if (name === void 0) return calls;
32951
+ if (hook.event === "PreToolUse") {
32952
+ if (callId === void 0) return calls;
32953
+ const open = new Map(calls.open).set(callId, at);
32954
+ for (const [oldest] of open) {
32955
+ if (open.size <= openCalls) break;
32956
+ open.delete(oldest);
32957
+ }
32958
+ return {
32959
+ ...calls,
32960
+ open
32961
+ };
32962
+ }
32963
+ if (hook.event !== "PostToolUse" && hook.event !== "PostToolUseFailure") return calls;
32964
+ const open = new Map(calls.open);
32965
+ const began = callId === void 0 ? void 0 : open.get(callId);
32966
+ if (callId !== void 0) open.delete(callId);
32967
+ const ms = hook.durationMs ?? (began === void 0 ? void 0 : Math.max(at - began, 0));
32968
+ if (ms === void 0 || name.length > 128) return {
32969
+ ...calls,
32970
+ known: false,
32971
+ open
32972
+ };
32973
+ const longest = calls.longest === void 0 || ms > calls.longest.ms ? {
32974
+ tool: name,
32975
+ ms
32976
+ } : calls.longest;
32977
+ return {
32978
+ ...calls,
32979
+ totalMs: calls.totalMs + ms,
32980
+ longest,
32981
+ open
32982
+ };
32983
+ }
32984
+ /**
32985
+ * A hook seen at `at`: a tool call counted by its tool once it ran, and timed, the files an edit
32986
+ * is about to touch kept, at most 50 and the rest counted, the model, where the hook names one,
32987
+ * and a `Stop` as when the agent last stopped.
32988
+ */
32989
+ function seeHook(seen, hook, edited, at) {
32990
+ const tools = new Map(seen.tools);
32991
+ const name = hook.tool?.name;
32992
+ if (hook.event === "PostToolUse" && name !== void 0) {
32993
+ const counted = tools.get(name);
32994
+ if (counted !== void 0 || tools.size < toolNames) tools.set(name, (counted ?? 0) + 1);
32995
+ }
32996
+ const addresses = [...seen.addresses];
32997
+ let more = seen.more;
32998
+ for (const each of edited) {
32999
+ if (addresses.some((kept) => kept.repo === each.repo && kept.path === each.path)) continue;
33000
+ if (addresses.length < 50) addresses.push(each);
33001
+ else more += 1;
33002
+ }
33003
+ return {
33004
+ ...seen,
33005
+ tools,
33006
+ calls: timeCall(seen.calls, hook, at),
33007
+ addresses,
33008
+ more,
33009
+ model: hook.model ?? seen.model,
33010
+ stoppedAt: hook.event === "Stop" ? at : seen.stoppedAt
33011
+ };
33012
+ }
33013
+ /**
33014
+ * A read's replies added to a turn's, each by its message ID at the most any of its lines says:
33015
+ * a subagent's transcript writes a reply's output as it grows, line by line, so a line of a reply
33016
+ * already counted adds only what it grew. `own` when the read was of the session's own
33017
+ * transcript, which makes the turn's tokens known; nothing makes them known once a read has
33018
+ * failed.
33019
+ */
33020
+ function addReplies(tally, replies, own) {
33021
+ if (tally.known === "failed") return tally;
33022
+ const models = new Map(tally.models);
33023
+ const counted = new Map(tally.replies);
33024
+ for (const reply of replies) {
33025
+ const before = counted.get(reply.id);
33026
+ const most = {
33027
+ model: before?.model ?? reply.model,
33028
+ input: Math.max(before?.input ?? 0, reply.input),
33029
+ output: Math.max(before?.output ?? 0, reply.output),
33030
+ cacheWrite: Math.max(before?.cacheWrite ?? 0, reply.cacheWrite),
33031
+ cacheRead: Math.max(before?.cacheRead ?? 0, reply.cacheRead)
33032
+ };
33033
+ const sum = models.get(most.model);
33034
+ counted.set(reply.id, most);
33035
+ models.set(most.model, {
33036
+ input: (sum?.input ?? 0) + most.input - (before?.input ?? 0),
33037
+ output: (sum?.output ?? 0) + most.output - (before?.output ?? 0),
33038
+ cacheWrite: (sum?.cacheWrite ?? 0) + most.cacheWrite - (before?.cacheWrite ?? 0),
33039
+ cacheRead: (sum?.cacheRead ?? 0) + most.cacheRead - (before?.cacheRead ?? 0)
33040
+ });
33041
+ }
33042
+ if (counted.size > turnReplies) return readFailed(tally);
33043
+ return {
33044
+ known: own ? "read" : tally.known,
33045
+ models,
33046
+ replies: counted
33047
+ };
33048
+ }
33049
+ /**
33050
+ * A read's requests added to a turn's, by model, as Codex's rollout steps its running total.
33051
+ * `own` as for `addReplies`: a rollout is always the session's own.
33052
+ */
33053
+ function addSteps(tally, steps, own) {
33054
+ if (tally.known === "failed") return tally;
33055
+ const models = new Map(tally.models);
33056
+ for (const step of steps) {
33057
+ const sum = models.get(step.model);
33058
+ models.set(step.model, {
33059
+ input: (sum?.input ?? 0) + step.input,
33060
+ output: (sum?.output ?? 0) + step.output,
33061
+ cacheWrite: (sum?.cacheWrite ?? 0) + step.cacheWrite,
33062
+ cacheRead: (sum?.cacheRead ?? 0) + step.cacheRead
33063
+ });
33064
+ }
33065
+ return {
33066
+ ...tally,
33067
+ known: own ? "read" : tally.known,
33068
+ models
33069
+ };
33070
+ }
33071
+ /** A read that failed: the turn's tokens are unknown from then on. */
33072
+ const readFailed = (tally) => ({
33073
+ ...tally,
33074
+ known: "failed"
33075
+ });
33076
+ /** A read's replies and requests added to a turn's tokens, or its tokens unknown if it failed. */
33077
+ const addTokens = (tally, read, own) => read.whole ? addSteps(addReplies(tally, read.replies, own), read.steps, own) : readFailed(tally);
33078
+ /** What one request sent: its input, cached or not. */
33079
+ const sentBy = (tokens) => tokens.input + tokens.cacheWrite + tokens.cacheRead;
33080
+ /**
33081
+ * A read of the session's own transcript added to its context: the most any request sent, each
33082
+ * Claude Code reply at the most its lines said, and each compaction. Unknown once a read fails.
33083
+ */
33084
+ function addContext(context, read) {
33085
+ if (context.known === "failed") return context;
33086
+ if (!read.whole) return {
33087
+ ...context,
33088
+ known: "failed"
33089
+ };
33090
+ return {
33091
+ known: "read",
33092
+ peak: [...read.replies, ...read.steps].reduce((most, request) => Math.max(most ?? 0, sentBy(request)), context.peak),
33093
+ compactions: [...context.compactions, ...read.compactions]
33094
+ };
33095
+ }
33096
+ /**
33097
+ * A read added to a turn's: to its tokens, and, from the session's own transcript, to its
33098
+ * context, or, from a subagent's, to that subagent's own tokens too, each subagent kept by its
33099
+ * transcript as first read, past the first 100 only counted.
33100
+ */
33101
+ function addRead(reads, read, of) {
33102
+ const tokens = addTokens(reads.tokens, read, of.own);
33103
+ if (of.own) return {
33104
+ ...reads,
33105
+ tokens,
33106
+ context: addContext(reads.context, read)
33107
+ };
33108
+ const before = reads.subagents.get(of.subagent);
33109
+ if (before === void 0 && reads.subagents.size >= 100) return {
33110
+ ...reads,
33111
+ tokens,
33112
+ moreSubagents: reads.moreSubagents + 1
33113
+ };
33114
+ const subagent = {
33115
+ type: before?.type ?? of.type,
33116
+ tokens: addTokens(before?.tokens ?? nothingRead, read, true)
33117
+ };
33118
+ return {
33119
+ ...reads,
33120
+ tokens,
33121
+ subagents: new Map(reads.subagents).set(of.subagent, subagent)
33122
+ };
33123
+ }
33124
+ /**
33125
+ * A turn's tokens as ref0 keeps them: each model's counts once the session's own transcript is
33126
+ * read, or unknown, with no counts, where it wasn't, a read failed, or a count passes what a
33127
+ * turn may hold.
33128
+ */
33129
+ function turnTokens(tally) {
33130
+ if (tally.known !== "read" || tally.models.size > 16) return { known: "unknown" };
33131
+ const models = [...tally.models].map(([model, counts]) => ({
33132
+ model,
33133
+ ...counts
33134
+ }));
33135
+ return models.every((each) => Math.max(each.input, each.output, each.cacheWrite, each.cacheRead) <= 1e8 && each.model.length >= 1 && each.model.length <= 128) ? {
33136
+ known: "read",
33137
+ models
33138
+ } : { known: "unknown" };
33139
+ }
33140
+ /** A turn's tool calls' time as ref0 keeps it: unknown once a call's time couldn't be told. */
33141
+ function toolTime(calls) {
33142
+ if (!calls.known) return { known: "unknown" };
33143
+ return calls.longest === void 0 ? {
33144
+ known: "timed",
33145
+ totalMs: calls.totalMs
33146
+ } : {
33147
+ known: "timed",
33148
+ totalMs: calls.totalMs,
33149
+ longest: calls.longest
33150
+ };
33151
+ }
33152
+ /**
33153
+ * A turn's context as ref0 keeps it, each compaction's time as how long after ref0 started the
33154
+ * turn, 0 for one before its start landed: unknown until the session's own transcript is read,
33155
+ * once a read fails, or with more compactions than a turn names.
33156
+ */
33157
+ function turnContext(context, startedAt) {
33158
+ if (context.known !== "read" || context.compactions.length > 100) return { known: "unknown" };
33159
+ if (context.peak !== void 0 && context.peak > 1e8) return { known: "unknown" };
33160
+ const compactions = context.compactions.map((each) => {
33161
+ const compaction = {};
33162
+ if (each.trigger !== void 0) compaction.trigger = each.trigger;
33163
+ if (each.at !== void 0 && startedAt !== void 0) compaction.afterMs = Math.max(each.at - startedAt, 0);
33164
+ return compaction;
33165
+ });
33166
+ return context.peak === void 0 ? {
33167
+ known: "read",
33168
+ compactions
33169
+ } : {
33170
+ known: "read",
33171
+ peak: context.peak,
33172
+ compactions
33173
+ };
33174
+ }
33175
+ /**
33176
+ * The facts a turn's reads give ref0: its tokens, its context and each subagent's tokens, with
33177
+ * its type where the harness named it.
33178
+ */
33179
+ const readFacts = (reads, startedAt) => ({
33180
+ tokens: turnTokens(reads.tokens),
33181
+ context: turnContext(reads.context, startedAt),
33182
+ subagents: [...reads.subagents.values()].map((subagent) => subagent.type === void 0 || subagent.type.length > 128 ? { tokens: turnTokens(subagent.tokens) } : {
33183
+ type: subagent.type,
33184
+ tokens: turnTokens(subagent.tokens)
33185
+ }),
33186
+ moreSubagents: reads.moreSubagents
33187
+ });
33188
+ /**
33189
+ * A turn's facts as ref0 keeps them, with its stop as how long after ref0 started it the agent last
33190
+ * stopped: none until both are seen, and 0 for a `Stop` before its start landed.
33191
+ */
33192
+ function turnFacts(seen) {
33193
+ const facts = {
33194
+ modelSource: seen.model === void 0 ? "unknown" : "hook",
33195
+ tools: Object.fromEntries(seen.tools),
33196
+ addresses: seen.addresses,
33197
+ more: seen.more,
33198
+ toolTime: toolTime(seen.calls),
33199
+ ...readFacts(seen, seen.startedAt)
33200
+ };
33201
+ if (seen.model !== void 0) facts.model = seen.model;
33202
+ if (seen.startedAt !== void 0 && seen.stoppedAt !== void 0) facts.stoppedAfterMs = Math.max(seen.stoppedAt - seen.startedAt, 0);
33203
+ return facts;
33204
+ }
33205
+ //#endregion
32378
33206
  //#region src/harness/rollouts.ts
32379
33207
  /**
32380
33208
  * Codex's rollouts, as `ref0 mcp` reads a turn's tokens from them: each hook names the
@@ -32394,7 +33222,8 @@ var HarnessTasks = class HarnessTasks extends Context.Service()("ref0/cli/Harnes
32394
33222
  * `token_count`s, then the subagent's own turn, its total from zero, a forked subagent's as a
32395
33223
  * fresh one's (Codex 0.163.0's subagents, 20 Oct 2026). So its requests are those past its own
32396
33224
  * turn's `task_started`, and what its total carried over, nothing so far, is its first request's
32397
- * total less that request's own usage.
33225
+ * total less that request's own usage. Each compaction of the session's context writes a
33226
+ * `compacted` record, with its time and none of what started it (Codex 0.163.0's, 23 Oct 2026).
32398
33227
  */
32399
33228
  const Count$1 = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
32400
33229
  /** A session's running total, as far as ref0 counts it: reasoning is inside output. */
@@ -32442,12 +33271,16 @@ const zero = {
32442
33271
  cacheWrite: 0,
32443
33272
  output: 0
32444
33273
  };
32445
- /** The records a read decodes: each request's total, each turn's model, start and cutting short. */
33274
+ /**
33275
+ * The records a read decodes: each request's total, each turn's model, start and cutting short,
33276
+ * and each compaction.
33277
+ */
32446
33278
  const counted = [
32447
33279
  "\"token_count\"",
32448
33280
  "\"turn_context\"",
32449
33281
  "\"task_started\"",
32450
- "\"turn_aborted\""
33282
+ "\"turn_aborted\"",
33283
+ "\"compacted\""
32451
33284
  ];
32452
33285
  /** A line, as far as telling Codex's records apart, and the turn an event is of. */
32453
33286
  const Line$1 = Schema.Struct({
@@ -32532,6 +33365,7 @@ const tokensOf = (model, step) => ({
32532
33365
  */
32533
33366
  function stepsIn(text, from, until) {
32534
33367
  const steps = [];
33368
+ const compactions = [];
32535
33369
  const lines = text.split("\n");
32536
33370
  let { top, total, model } = from;
32537
33371
  let whole = true;
@@ -32548,6 +33382,7 @@ function stepsIn(text, from, until) {
32548
33382
  const event = kind.value.type === "event_msg" ? kind.value.payload : void 0;
32549
33383
  if (event?.type === "task_started" && until !== void 0 && event.turn_id === until) return {
32550
33384
  steps,
33385
+ compactions,
32551
33386
  running: {
32552
33387
  top,
32553
33388
  total,
@@ -32556,6 +33391,11 @@ function stepsIn(text, from, until) {
32556
33391
  whole,
32557
33392
  before: index
32558
33393
  };
33394
+ if (kind.value.type === "compacted") {
33395
+ const at = writtenAt(line);
33396
+ compactions.push(at === void 0 ? {} : { at });
33397
+ continue;
33398
+ }
32559
33399
  if (event?.type === "turn_aborted") {
32560
33400
  whole = false;
32561
33401
  continue;
@@ -32586,6 +33426,7 @@ function stepsIn(text, from, until) {
32586
33426
  }
32587
33427
  return {
32588
33428
  steps,
33429
+ compactions,
32589
33430
  running: {
32590
33431
  top,
32591
33432
  total,
@@ -32603,7 +33444,9 @@ function stepsIn(text, from, until) {
32603
33444
  * Each holds one JSON object a line, and writes an assistant reply over several lines, each with
32604
33445
  * the reply's `message.usage` so far: the session's own repeat it, and a subagent's grow its
32605
33446
  * output line by line. So a reply counts once, by its `message.id`, at the most its lines say,
32606
- * with its own model (Claude Code 2.1.304's transcripts, 20 Oct 2026). Codex's: the session's
33447
+ * with its own model (Claude Code 2.1.304's transcripts, 20 Oct 2026), and a compaction as a
33448
+ * `system` line, `compact_boundary`, with its time and its trigger (2.1.310's, 24 Oct 2026).
33449
+ * Codex's: the session's
32607
33450
  * rollout, whose running total `rollouts.ts` reads, kept with its place, and each subagent's,
32608
33451
  * which its `SubagentStop` names, read first from where its own turn starts, past the parent's
32609
33452
  * history it copies. Each is read from the
@@ -32629,8 +33472,13 @@ const linesEnd = (bytes, count) => {
32629
33472
  return end;
32630
33473
  };
32631
33474
  const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
32632
- /** A line, as far as telling an assistant's from the rest. */
32633
- const Line = Schema.Struct({ type: Schema.String });
33475
+ /** A line, as far as telling an assistant's and a compaction's from the rest. */
33476
+ const Line = Schema.Struct({
33477
+ type: Schema.String,
33478
+ subtype: Schema.optionalKey(Schema.Unknown)
33479
+ });
33480
+ /** A compaction's line, as far as what started it. */
33481
+ const CompactionLine = Schema.Struct({ compactMetadata: Schema.Struct({ trigger: Schema.Literals(["auto", "manual"]) }) });
32634
33482
  /** An assistant line: a reply's ID, its model and its usage, which every line of it repeats. */
32635
33483
  const AssistantLine = Schema.Struct({ message: Schema.Struct({
32636
33484
  model: Schema.String,
@@ -32644,18 +33492,34 @@ const AssistantLine = Schema.Struct({ message: Schema.Struct({
32644
33492
  }) });
32645
33493
  const decodeLine = Schema.decodeUnknownOption(Schema.fromJsonString(Line));
32646
33494
  const decodeAssistant = Schema.decodeUnknownOption(Schema.fromJsonString(AssistantLine));
33495
+ const decodeCompaction = Schema.decodeUnknownOption(Schema.fromJsonString(CompactionLine));
33496
+ /** A compaction's line read: one is counted whatever else it says. */
33497
+ const compactionOf = (line) => {
33498
+ const trigger = Option.getOrUndefined(decodeCompaction(line))?.compactMetadata.trigger;
33499
+ const at = writtenAt(line);
33500
+ const compaction = {};
33501
+ if (trigger !== void 0) compaction.trigger = trigger;
33502
+ if (at !== void 0) compaction.at = at;
33503
+ return compaction;
33504
+ };
32647
33505
  /** What Claude Code names the model of a reply it wrote itself, such as an error: no model's. */
32648
33506
  const synthetic = "<synthetic>";
32649
33507
  /**
32650
- * The replies in a transcript's whole lines, in order, or none if an assistant line can't be read,
32651
- * since its tokens would go missing unseen. A line that isn't JSON is passed over: a read that
32652
- * starts inside a line, after a transcript shrank, reads its tail as one.
33508
+ * The replies and compactions in a transcript's whole lines, in order, or none if an assistant
33509
+ * line can't be read, since its tokens would go missing unseen. A line that isn't JSON is passed
33510
+ * over: a read that starts inside a line, after a transcript shrank, reads its tail as one.
32653
33511
  */
32654
- function repliesIn(text) {
33512
+ function transcriptIn(text) {
32655
33513
  const replies = [];
33514
+ const compactions = [];
32656
33515
  for (const line of text.split("\n")) {
32657
33516
  const kind = decodeLine(line);
32658
- if (Option.isNone(kind) || kind.value.type !== "assistant") continue;
33517
+ if (Option.isNone(kind)) continue;
33518
+ if (kind.value.type === "system" && kind.value.subtype === "compact_boundary") {
33519
+ compactions.push(compactionOf(line));
33520
+ continue;
33521
+ }
33522
+ if (kind.value.type !== "assistant") continue;
32659
33523
  const assistant = decodeAssistant(line);
32660
33524
  if (Option.isNone(assistant)) return Option.none();
32661
33525
  const { model, id, usage } = assistant.value.message;
@@ -32670,8 +33534,17 @@ function repliesIn(text) {
32670
33534
  cacheRead: usage.cache_read_input_tokens ?? 0
32671
33535
  });
32672
33536
  }
32673
- return Option.some(replies);
33537
+ return Option.some({
33538
+ replies,
33539
+ compactions
33540
+ });
32674
33541
  }
33542
+ /** A read that found nothing. */
33543
+ const nothingIn = {
33544
+ replies: [],
33545
+ steps: [],
33546
+ compactions: []
33547
+ };
32675
33548
  /** A transcript never read: its top. */
32676
33549
  const top = {
32677
33550
  offset: 0,
@@ -32704,6 +33577,23 @@ const KeptCounts = Schema.Struct({
32704
33577
  cacheWrite: Count,
32705
33578
  cacheRead: Count
32706
33579
  });
33580
+ const Known = Schema.Literals([
33581
+ "waiting",
33582
+ "read",
33583
+ "failed"
33584
+ ]);
33585
+ const KeptTally = Schema.Struct({
33586
+ known: Known,
33587
+ models: Schema.Array(Schema.Struct({
33588
+ model: Schema.String,
33589
+ ...KeptCounts.fields
33590
+ })),
33591
+ replies: Schema.Array(Schema.Struct({
33592
+ id: Schema.String,
33593
+ model: Schema.String,
33594
+ ...KeptCounts.fields
33595
+ }))
33596
+ });
32707
33597
  const KeptFile = Schema.Struct({
32708
33598
  generation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
32709
33599
  read: Schema.Array(Schema.Struct({
@@ -32716,24 +33606,42 @@ const KeptFile = Schema.Struct({
32716
33606
  turn: Schema.optionalKey(Schema.Struct({
32717
33607
  number: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
32718
33608
  facts: TurnFacts,
32719
- known: Schema.Literals([
32720
- "waiting",
32721
- "read",
32722
- "failed"
32723
- ]),
32724
- models: Schema.Array(Schema.Struct({
32725
- model: Schema.String,
32726
- ...KeptCounts.fields
33609
+ startedAt: Schema.optionalKey(Count),
33610
+ tokens: KeptTally,
33611
+ context: Schema.Struct({
33612
+ known: Known,
33613
+ peak: Schema.optionalKey(Count),
33614
+ compactions: Schema.Array(Schema.Struct({
33615
+ trigger: Schema.optionalKey(Schema.Literals(["auto", "manual"])),
33616
+ at: Schema.optionalKey(Count)
33617
+ }))
33618
+ }),
33619
+ subagents: Schema.Array(Schema.Struct({
33620
+ transcript: Schema.String,
33621
+ type: Schema.optionalKey(Schema.String),
33622
+ tokens: KeptTally
32727
33623
  })),
32728
- replies: Schema.Array(Schema.Struct({
32729
- id: Schema.String,
32730
- model: Schema.String,
32731
- ...KeptCounts.fields
32732
- }))
33624
+ moreSubagents: Count
32733
33625
  }))
32734
33626
  });
32735
33627
  const encodeKept = Schema.encodeSync(Schema.fromJsonString(KeptFile));
32736
33628
  const decodeKept = Schema.decodeUnknownOption(Schema.fromJsonString(KeptFile));
33629
+ const tallyFromFile = (tally) => ({
33630
+ known: tally.known,
33631
+ models: new Map(tally.models.map(({ model, ...counts }) => [model, counts])),
33632
+ replies: new Map(tally.replies.map(({ id, ...reply }) => [id, reply]))
33633
+ });
33634
+ const tallyToFile = (tally) => ({
33635
+ known: tally.known,
33636
+ models: [...tally.models].map(([model, counts]) => ({
33637
+ model,
33638
+ ...counts
33639
+ })),
33640
+ replies: [...tally.replies].map(([id, reply]) => ({
33641
+ id,
33642
+ ...reply
33643
+ }))
33644
+ });
32737
33645
  const fromFile = (file) => ({
32738
33646
  generation: file.generation,
32739
33647
  places: new Map(file.read.map(({ path, ...place }) => [path, place])),
@@ -32741,10 +33649,19 @@ const fromFile = (file) => ({
32741
33649
  turn: Option.fromNullishOr(file.turn).pipe(Option.map((turn) => ({
32742
33650
  number: turn.number,
32743
33651
  facts: turn.facts,
32744
- tokens: {
32745
- known: turn.known,
32746
- models: new Map(turn.models.map(({ model, ...counts }) => [model, counts])),
32747
- replies: new Map(turn.replies.map(({ id, ...reply }) => [id, reply]))
33652
+ startedAt: turn.startedAt,
33653
+ reads: {
33654
+ tokens: tallyFromFile(turn.tokens),
33655
+ context: {
33656
+ known: turn.context.known,
33657
+ peak: turn.context.peak,
33658
+ compactions: turn.context.compactions
33659
+ },
33660
+ subagents: new Map(turn.subagents.map(({ transcript, type, tokens }) => [transcript, {
33661
+ type,
33662
+ tokens: tallyFromFile(tokens)
33663
+ }])),
33664
+ moreSubagents: turn.moreSubagents
32748
33665
  }
32749
33666
  })))
32750
33667
  });
@@ -32758,20 +33675,32 @@ const toFile = (kept) => {
32758
33675
  own: kept.own
32759
33676
  };
32760
33677
  if (Option.isSome(kept.turn)) {
32761
- const { number, facts, tokens } = kept.turn.value;
32762
- file.turn = {
33678
+ const { number, facts, reads, startedAt } = kept.turn.value;
33679
+ const { known, peak, compactions } = reads.context;
33680
+ const turn = {
32763
33681
  number,
32764
33682
  facts,
32765
- known: tokens.known,
32766
- models: [...tokens.models].map(([model, counts]) => ({
32767
- model,
32768
- ...counts
32769
- })),
32770
- replies: [...tokens.replies].map(([id, reply]) => ({
32771
- id,
32772
- ...reply
32773
- }))
33683
+ tokens: tallyToFile(reads.tokens),
33684
+ context: peak === void 0 ? {
33685
+ known,
33686
+ compactions
33687
+ } : {
33688
+ known,
33689
+ peak,
33690
+ compactions
33691
+ },
33692
+ subagents: [...reads.subagents].map(([transcript, { type, tokens }]) => type === void 0 ? {
33693
+ transcript,
33694
+ tokens: tallyToFile(tokens)
33695
+ } : {
33696
+ transcript,
33697
+ type,
33698
+ tokens: tallyToFile(tokens)
33699
+ }),
33700
+ moreSubagents: reads.moreSubagents
32774
33701
  };
33702
+ if (startedAt !== void 0) turn.startedAt = startedAt;
33703
+ file.turn = turn;
32775
33704
  }
32776
33705
  return file;
32777
33706
  };
@@ -32868,8 +33797,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32868
33797
  const readOne = Effect.fnUntraced(function* (harness, transcript, from, until) {
32869
33798
  const info = yield* fs.stat(transcript);
32870
33799
  if (info.type !== "File") return {
32871
- replies: [],
32872
- steps: [],
33800
+ ...nothingIn,
32873
33801
  whole: false,
32874
33802
  to: from,
32875
33803
  reached: false
@@ -32881,6 +33809,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32881
33809
  const lost = harness === "codex" && (!same || from.running === void 0);
32882
33810
  const replies = [];
32883
33811
  const steps = [];
33812
+ const compactions = [];
32884
33813
  let at = lost ? yield* tailOf(file, size) : same ? from.offset : 0;
32885
33814
  let running = lost ? totalLost : from.running;
32886
33815
  let whole = !lost;
@@ -32899,6 +33828,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32899
33828
  if (harness === "codex") {
32900
33829
  const found = stepsIn(text, running ?? totalLost, until);
32901
33830
  steps.push(...found.steps);
33831
+ compactions.push(...found.compactions);
32902
33832
  running = found.running;
32903
33833
  whole &&= found.whole;
32904
33834
  if (found.before !== void 0) {
@@ -32907,8 +33837,11 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32907
33837
  break;
32908
33838
  }
32909
33839
  } else {
32910
- const found = repliesIn(text);
32911
- replies.push(...Option.getOrElse(found, () => []));
33840
+ const found = transcriptIn(text);
33841
+ if (Option.isSome(found)) {
33842
+ replies.push(...found.value.replies);
33843
+ compactions.push(...found.value.compactions);
33844
+ }
32912
33845
  whole &&= Option.isSome(found);
32913
33846
  }
32914
33847
  at += lines;
@@ -32917,6 +33850,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32917
33850
  return {
32918
33851
  replies,
32919
33852
  steps,
33853
+ compactions,
32920
33854
  whole,
32921
33855
  to: placeOf(at, hash, running),
32922
33856
  reached
@@ -32969,25 +33903,27 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32969
33903
  const state = yield* stateOf(owner);
32970
33904
  const replies = [];
32971
33905
  const steps = [];
33906
+ const compactions = [];
32972
33907
  const places = /* @__PURE__ */ new Map();
32973
33908
  let whole = true;
32974
33909
  for (const transcript of transcripts) {
32975
33910
  const since = harness === "codex" ? turn?.since : void 0;
32976
33911
  const from = state.places.get(transcript) ?? (since === void 0 ? top : yield* subagentStartOf(transcript, since));
32977
33912
  const one = yield* readOne(harness, transcript, from, turn?.until).pipe(Effect.orElseSucceed(() => ({
32978
- replies: [],
32979
- steps: [],
33913
+ ...nothingIn,
32980
33914
  whole: false,
32981
33915
  to: from
32982
33916
  })));
32983
33917
  replies.push(...one.replies);
32984
33918
  steps.push(...one.steps);
33919
+ compactions.push(...one.compactions);
32985
33920
  places.set(transcript, one.to);
32986
33921
  whole &&= one.whole;
32987
33922
  }
32988
33923
  return {
32989
33924
  replies,
32990
33925
  steps,
33926
+ compactions,
32991
33927
  places,
32992
33928
  whole
32993
33929
  };
@@ -33172,6 +34108,8 @@ function withContext(answer, event, delivered) {
33172
34108
  //#region src/harness/hook-input.ts
33173
34109
  const decodeObjectText = Schema.decodeUnknownOption(Schema.fromJsonString(JsonObject));
33174
34110
  const filled = (text) => text === "" || text === null ? void 0 : text;
34111
+ /** Milliseconds as a hook's text says them, rounded; none for text that isn't a number. */
34112
+ const millisOf = (text) => text !== void 0 && /^\d{1,15}(\.\d+)?$/.test(text) ? Math.round(Number(text)) : void 0;
33175
34113
  const toolValue = (value) => {
33176
34114
  if (value === void 0 || value === "") return void 0;
33177
34115
  if (value instanceof Object) return value;
@@ -33185,22 +34123,26 @@ function readHook(args) {
33185
34123
  };
33186
34124
  const turn = filled(args.prompt_id) ?? filled(args.turn_id);
33187
34125
  const agent = filled(args.agent_id);
34126
+ const agentType = filled(args.agent_type);
33188
34127
  const name = filled(args.tool_name);
33189
34128
  const callId = filled(args.tool_use_id);
33190
34129
  const input = toolValue(args.tool_input);
33191
34130
  const response = toolValue(args.tool_response);
34131
+ const durationMs = millisOf(filled(args.duration_ms));
33192
34132
  const model = filled(args.model);
33193
34133
  const cwd = filled(args.cwd);
33194
34134
  const transcript = filled(args.transcript_path);
33195
34135
  const agentTranscript = filled(args.agent_transcript_path);
33196
34136
  if (turn !== void 0) hook.harnessTurn = turn;
33197
34137
  if (agent !== void 0) hook.agent = agent;
34138
+ if (agentType !== void 0) hook.agentType = agentType;
33198
34139
  if (name !== void 0) hook.tool = callId === void 0 ? { name } : {
33199
34140
  name,
33200
34141
  callId
33201
34142
  };
33202
34143
  if (input instanceof Object) hook.toolInput = input;
33203
34144
  if (response !== void 0) hook.toolResponse = response;
34145
+ if (durationMs !== void 0) hook.durationMs = durationMs;
33204
34146
  if (model !== void 0) hook.model = model;
33205
34147
  if (cwd !== void 0) hook.cwd = cwd;
33206
34148
  if (transcript !== void 0) hook.transcript = transcript;
@@ -33257,11 +34199,11 @@ const whyNotSaidYet = "Not said yet: the agent says why this thread is needed, a
33257
34199
  const startHarnessSession = Effect.fnUntraced(function* (input) {
33258
34200
  const hostGrant = yield* HostGrant;
33259
34201
  const keychain = yield* Keychain;
33260
- const config = yield* Ref0Config;
33261
34202
  const harness = input.binding.harness === "codex" ? "codex" : "claude-code";
33262
34203
  const folder = yield* (yield* ConfigFolders).folder(harness);
33263
34204
  const grant = yield* hostGrant.get(folder);
33264
34205
  if (Option.isNone(grant) || grant.value.harness !== harness) return Option.none();
34206
+ const client = yield* grantClient(grant.value);
33265
34207
  const pending = pendingAccount(input.binding);
33266
34208
  const held = Option.flatMap(yield* keychain.get(sessionAccount(input.binding)), (kept) => parseToken(Redacted.value(kept)));
33267
34209
  const resent = Option.filter(Option.flatMap(yield* keychain.get(pending), (kept) => decodePending(Redacted.value(kept))), (one) => !Option.exists(held, (parts) => parts.secret === one.secret));
@@ -33296,7 +34238,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
33296
34238
  trigger: input.trigger,
33297
34239
  attended: "reported"
33298
34240
  });
33299
- const started = yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)))).sessions.start({
34241
+ const started = yield* client.sessions.start({
33300
34242
  headers: { "idempotency-key": requestId },
33301
34243
  payload
33302
34244
  }).pipe(Effect.tapError((error) => Match.value(error).pipe(Match.tag("ProjectNotFound", "ProjectNotReady", "ThreadNotFound", "ThreadIsClosed", "TextRefused", "NotPermitted", "DeliveryOutsideWindow", () => keychain.remove(pending)), Match.orElse(() => Effect.void))));
@@ -33626,175 +34568,53 @@ const sessionTasks = Effect.gen(function* () {
33626
34568
  };
33627
34569
  });
33628
34570
  //#endregion
33629
- //#region src/harness/turn-facts.ts
33630
- /** The most tool names a turn's record counts. */
33631
- const toolNames = 100;
33632
- /** The most replies a turn's tally remembers; a turn with more has its tokens unknown. */
33633
- const turnReplies = 1e4;
33634
- const nothingSeen = {
33635
- tools: /* @__PURE__ */ new Map(),
33636
- addresses: [],
33637
- more: 0,
33638
- model: void 0,
33639
- tokens: {
33640
- known: "waiting",
33641
- models: /* @__PURE__ */ new Map(),
33642
- replies: /* @__PURE__ */ new Map()
33643
- },
33644
- startedAt: void 0,
33645
- stoppedAt: void 0
33646
- };
33647
- /**
33648
- * A hook seen at `at`: a tool call counted by its tool once it ran, the files an edit is about to
33649
- * touch kept, at most 50 and the rest counted, the model, where the hook names one, and a `Stop`
33650
- * as when the agent last stopped.
33651
- */
33652
- function seeHook(seen, hook, edited, at) {
33653
- const tools = new Map(seen.tools);
33654
- const name = hook.tool?.name;
33655
- if (hook.event === "PostToolUse" && name !== void 0) {
33656
- const counted = tools.get(name);
33657
- if (counted !== void 0 || tools.size < toolNames) tools.set(name, (counted ?? 0) + 1);
33658
- }
33659
- const addresses = [...seen.addresses];
33660
- let more = seen.more;
33661
- for (const each of edited) {
33662
- if (addresses.some((kept) => kept.repo === each.repo && kept.path === each.path)) continue;
33663
- if (addresses.length < 50) addresses.push(each);
33664
- else more += 1;
33665
- }
33666
- return {
33667
- ...seen,
33668
- tools,
33669
- addresses,
33670
- more,
33671
- model: hook.model ?? seen.model,
33672
- stoppedAt: hook.event === "Stop" ? at : seen.stoppedAt
33673
- };
33674
- }
33675
- /**
33676
- * A read's replies added to a turn's, each by its message ID at the most any of its lines says:
33677
- * a subagent's transcript writes a reply's output as it grows, line by line, so a line of a reply
33678
- * already counted adds only what it grew. `own` when the read was of the session's own
33679
- * transcript, which makes the turn's tokens known; nothing makes them known once a read has
33680
- * failed.
33681
- */
33682
- function addReplies(tally, replies, own) {
33683
- if (tally.known === "failed") return tally;
33684
- const models = new Map(tally.models);
33685
- const counted = new Map(tally.replies);
33686
- for (const reply of replies) {
33687
- const before = counted.get(reply.id);
33688
- const most = {
33689
- model: before?.model ?? reply.model,
33690
- input: Math.max(before?.input ?? 0, reply.input),
33691
- output: Math.max(before?.output ?? 0, reply.output),
33692
- cacheWrite: Math.max(before?.cacheWrite ?? 0, reply.cacheWrite),
33693
- cacheRead: Math.max(before?.cacheRead ?? 0, reply.cacheRead)
33694
- };
33695
- const sum = models.get(most.model);
33696
- counted.set(reply.id, most);
33697
- models.set(most.model, {
33698
- input: (sum?.input ?? 0) + most.input - (before?.input ?? 0),
33699
- output: (sum?.output ?? 0) + most.output - (before?.output ?? 0),
33700
- cacheWrite: (sum?.cacheWrite ?? 0) + most.cacheWrite - (before?.cacheWrite ?? 0),
33701
- cacheRead: (sum?.cacheRead ?? 0) + most.cacheRead - (before?.cacheRead ?? 0)
33702
- });
33703
- }
33704
- if (counted.size > turnReplies) return readFailed(tally);
33705
- return {
33706
- known: own ? "read" : tally.known,
33707
- models,
33708
- replies: counted
33709
- };
33710
- }
33711
- /**
33712
- * A read's requests added to a turn's, by model, as Codex's rollout steps its running total.
33713
- * `own` as for `addReplies`: a rollout is always the session's own.
33714
- */
33715
- function addSteps(tally, steps, own) {
33716
- if (tally.known === "failed") return tally;
33717
- const models = new Map(tally.models);
33718
- for (const step of steps) {
33719
- const sum = models.get(step.model);
33720
- models.set(step.model, {
33721
- input: (sum?.input ?? 0) + step.input,
33722
- output: (sum?.output ?? 0) + step.output,
33723
- cacheWrite: (sum?.cacheWrite ?? 0) + step.cacheWrite,
33724
- cacheRead: (sum?.cacheRead ?? 0) + step.cacheRead
33725
- });
33726
- }
33727
- return {
33728
- ...tally,
33729
- known: own ? "read" : tally.known,
33730
- models
33731
- };
33732
- }
33733
- /** A read that failed: the turn's tokens are unknown from then on. */
33734
- const readFailed = (tally) => ({
33735
- ...tally,
33736
- known: "failed"
33737
- });
33738
- /** A read's replies and requests added to a turn's tokens, or its tokens unknown if it failed. */
33739
- const addRead = (tally, read, own) => read.whole ? addSteps(addReplies(tally, read.replies, own), read.steps, own) : readFailed(tally);
33740
- /**
33741
- * A turn's tokens as ref0 keeps them: each model's counts once the session's own transcript is
33742
- * read, or unknown, with no counts, where it wasn't, a read failed, or a count passes what a
33743
- * turn may hold.
33744
- */
33745
- function turnTokens(tally) {
33746
- if (tally.known !== "read" || tally.models.size > 16) return { known: "unknown" };
33747
- const models = [...tally.models].map(([model, counts]) => ({
33748
- model,
33749
- ...counts
33750
- }));
33751
- return models.every((each) => Math.max(each.input, each.output, each.cacheWrite, each.cacheRead) <= 1e8 && each.model.length >= 1 && each.model.length <= 128) ? {
33752
- known: "read",
33753
- models
33754
- } : { known: "unknown" };
33755
- }
33756
- /**
33757
- * A turn's facts as ref0 keeps them, with its stop as how long after ref0 started it the agent last
33758
- * stopped: none until both are seen, and 0 for a `Stop` before its start landed.
33759
- */
33760
- function turnFacts(seen) {
33761
- const facts = {
33762
- modelSource: seen.model === void 0 ? "unknown" : "hook",
33763
- tools: Object.fromEntries(seen.tools),
33764
- addresses: seen.addresses,
33765
- more: seen.more,
33766
- tokens: turnTokens(seen.tokens)
33767
- };
33768
- if (seen.model !== void 0) facts.model = seen.model;
33769
- if (seen.startedAt !== void 0 && seen.stoppedAt !== void 0) facts.stoppedAfterMs = Math.max(seen.stoppedAt - seen.startedAt, 0);
33770
- return facts;
33771
- }
33772
- //#endregion
33773
34571
  //#region src/harness/session-turns.ts
33774
34572
  /** The open turn as `SessionEnd` tells it once more: none until ref0 has numbered it. */
33775
34573
  const keptOf = (now) => now.kind === "open" ? Option.some({
33776
34574
  number: now.number,
33777
34575
  facts: turnFacts(now.seen),
33778
- tokens: now.seen.tokens
34576
+ reads: now.seen,
34577
+ startedAt: now.seen.startedAt
33779
34578
  }) : Option.none();
33780
34579
  /**
33781
- * The transcripts a hook's read takes, and whether the session's own is among them: at the
33782
- * tracked turn's own `Stop`, the session's; at any subagent's `SubagentStop`, its own, whichever
33783
- * turn started it, since it adds to the turn still open, with the turn Codex names there, the
33784
- * subagent's own, where its rollout's own part starts. A new turn's prompt reads the session's
33785
- * first, for the turn it ends.
34580
+ * The transcripts a hook's read takes, and whose they are: at the tracked turn's own `Stop`, the
34581
+ * session's; at any subagent's `SubagentStop`, its own, whichever turn started it, since it adds
34582
+ * to the turn still open, with the turn Codex names there, the subagent's own, where its
34583
+ * rollout's own part starts, and its type. A new turn's prompt reads the session's first, for
34584
+ * the turn it ends.
33786
34585
  */
33787
34586
  const toRead = (hook, ownHook) => {
33788
34587
  if (hook.event === "Stop" && ownHook && hook.transcript !== void 0) return {
33789
34588
  transcripts: [hook.transcript],
33790
- own: true
34589
+ of: { own: true }
33791
34590
  };
33792
34591
  if (hook.event === "SubagentStop" && hook.agentTranscript !== void 0) return {
33793
34592
  transcripts: [hook.agentTranscript],
33794
- own: false,
34593
+ of: {
34594
+ own: false,
34595
+ subagent: hook.agentTranscript,
34596
+ type: hook.agentType
34597
+ },
33795
34598
  turn: { since: hook.harnessTurn }
33796
34599
  };
33797
34600
  };
34601
+ /** A read that couldn't happen, as of a subagent whose harness kept no transcript of it. */
34602
+ const unread = {
34603
+ replies: [],
34604
+ steps: [],
34605
+ compactions: [],
34606
+ whole: false
34607
+ };
34608
+ /**
34609
+ * A subagent's `SubagentStop` that names no transcript of it, as Codex's in a session kept
34610
+ * nowhere: the subagent counted by its ID, with its type, and its tokens, and so the turn's,
34611
+ * unknown, since what it used can't be read.
34612
+ */
34613
+ const untranscribed = (hook) => hook.event === "SubagentStop" && hook.agentTranscript === void 0 ? {
34614
+ own: false,
34615
+ subagent: `agent:${hook.agent ?? ""}`,
34616
+ type: hook.agentType
34617
+ } : void 0;
33798
34618
  /** A harness session's turns, each numbered by ref0 as it starts. */
33799
34619
  const sessionTurns = Effect.fnUntraced(function* (edges) {
33800
34620
  const transcripts = yield* Transcripts;
@@ -33815,7 +34635,11 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33815
34635
  const ending = prompted === void 0 ? void 0 : yield* transcripts.read(owner, harness, [prompted], { until: harnessTurn });
33816
34636
  const fresh = late && harness === "claude-code" ? {
33817
34637
  ...nothingSeen,
33818
- tokens: readFailed(nothingSeen.tokens)
34638
+ tokens: readFailed(nothingSeen.tokens),
34639
+ context: {
34640
+ ...nothingSeen.context,
34641
+ known: "failed"
34642
+ }
33819
34643
  } : nothingSeen;
33820
34644
  const tracked = isNew ? Option.some({
33821
34645
  kind: "starting",
@@ -33824,7 +34648,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33824
34648
  turn: now.number,
33825
34649
  seen: ending === void 0 ? now.seen : {
33826
34650
  ...now.seen,
33827
- tokens: addRead(now.seen.tokens, ending, true)
34651
+ ...addRead(now.seen, ending, { own: true })
33828
34652
  }
33829
34653
  }) : now.previous),
33830
34654
  seen: fresh
@@ -33836,9 +34660,13 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33836
34660
  const hooked = ownHook && !isNew ? seeHook(tracked.value.seen, hook, edited, yield* Clock.currentTimeMillis) : tracked.value.seen;
33837
34661
  const reading = toRead(hook, ownHook);
33838
34662
  const read = reading === void 0 ? void 0 : yield* transcripts.read(owner, harness, reading.transcripts, reading.turn);
33839
- const seen = read === void 0 || reading === void 0 ? hooked : {
34663
+ const unseen = untranscribed(hook);
34664
+ const seen = read === void 0 || reading === void 0 ? unseen === void 0 ? hooked : {
34665
+ ...hooked,
34666
+ ...addRead(hooked, unread, unseen)
34667
+ } : {
33840
34668
  ...hooked,
33841
- tokens: addRead(hooked.tokens, read, reading.own)
34669
+ ...addRead(hooked, read, reading.of)
33842
34670
  };
33843
34671
  const places = (read ?? ending)?.places;
33844
34672
  let now = {
@@ -33847,7 +34675,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33847
34675
  };
33848
34676
  yield* Effect.uninterruptible(Effect.gen(function* () {
33849
34677
  yield* Ref.set(turn, Option.some(now));
33850
- if (places !== void 0) yield* transcripts.keep(owner, places, keptOf(now));
34678
+ if (places !== void 0 || unseen !== void 0) yield* transcripts.keep(owner, places ?? /* @__PURE__ */ new Map(), keptOf(now));
33851
34679
  }));
33852
34680
  if (now.kind === "starting") {
33853
34681
  if (Option.isSome(now.previous)) yield* edges.tasksLanded(client, bound, deadline);
@@ -33898,7 +34726,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33898
34726
  if (ownHook && hook.event === "Stop") {
33899
34727
  yield* edges.tasksLanded(client, bound, deadline);
33900
34728
  yield* tellFacts;
33901
- } else if (hook.event === "SubagentStop" && read !== void 0) yield* tellFacts;
34729
+ } else if (hook.event === "SubagentStop" && (read !== void 0 || unseen !== void 0)) yield* tellFacts;
33902
34730
  });
33903
34731
  return {
33904
34732
  now: Ref.get(turn),
@@ -34302,7 +35130,7 @@ const endWithin = "5 seconds";
34302
35130
  const lastReadWithin = "2 seconds";
34303
35131
  /**
34304
35132
  * The turn `ref0 mcp` last told ref0 of, told once more with what the session's transcript
34305
- * gained since: a reply written after its last `Stop` counts in it, and a `Stop` whose facts
35133
+ * gained since: a reply or a compaction written after its last `Stop` counts in it, and a `Stop` whose facts
34306
35134
  * didn't land has them sent again. Nothing for a turn ref0 hasn't numbered.
34307
35135
  */
34308
35136
  const lastRead = Effect.fnUntraced(function* (client, session, harness, transcript) {
@@ -34315,7 +35143,7 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
34315
35143
  };
34316
35144
  const turn = kept.value.turn.value;
34317
35145
  const read = transcript === null || transcript === void 0 || transcript === "" ? void 0 : yield* transcripts.read(owner, harness, [transcript]);
34318
- const tokens = read === void 0 ? turn.tokens : addRead(turn.tokens, read, true);
35146
+ const reads = read === void 0 ? turn.reads : addRead(turn.reads, read, { own: true });
34319
35147
  yield* client.sessions.turnFacts({
34320
35148
  params: {
34321
35149
  session,
@@ -34325,7 +35153,7 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
34325
35153
  generation: owner.generation,
34326
35154
  facts: {
34327
35155
  ...turn.facts,
34328
- tokens: turnTokens(tokens)
35156
+ ...readFacts(reads, turn.startedAt)
34329
35157
  }
34330
35158
  }
34331
35159
  });
@@ -34872,25 +35700,35 @@ const wordsOf = (report, home) => {
34872
35700
  }, ...check.next === void 0 ? [] : [{ command: check.next }]])];
34873
35701
  };
34874
35702
  const refused = () => Effect.succeed({ kind: "refused" });
34875
- const whoIs = Effect.fnUntraced(function* (token) {
34876
- const config = yield* Ref0Config;
34877
- return yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)))).me.get().pipe(Effect.timeout("10 seconds"), Effect.map((me) => ({
34878
- kind: "accepted",
34879
- me
34880
- })), Effect.catchTags({
34881
- Unauthenticated: refused,
34882
- SessionHasEnded: refused
34883
- }), Effect.orElseSucceed(() => ({ kind: "unanswered" })));
34884
- });
35703
+ const unsigned = (failed) => Effect.succeed({
35704
+ kind: "unsigned",
35705
+ nextStep: failed.nextStep
35706
+ });
35707
+ /** Asks ref0 who a client's credential is. */
35708
+ const whoIs = (client) => client.me.get().pipe(Effect.timeout("10 seconds"), Effect.map((me) => ({
35709
+ kind: "accepted",
35710
+ me
35711
+ })), Effect.catchTags({
35712
+ Unauthenticated: refused,
35713
+ SessionHasEnded: refused,
35714
+ InvalidDpopProof: refused,
35715
+ KeyStoreUnavailable: unsigned
35716
+ }), Effect.orElseSucceed(() => ({ kind: "unanswered" })));
35717
+ /** A session's token, as a bearer token: sessions aren't bound to a key yet. */
35718
+ const sessionClient = (token) => Effect.flatMap(Ref0Config, (config) => ref0Client(config.api).pipe(Effect.provide(bearerCredential(token))));
34885
35719
  const unansweredWords = (api) => `ref0 didn’t answer at ${api}.`;
34886
35720
  /** Check the grant kept for this folder, without borrowing another folder's authority. */
34887
35721
  const grantChecks = Effect.fnUntraced(function* (grant, configFolder) {
34888
- const asked = yield* whoIs(grant.token);
35722
+ const asked = yield* grantClient(grant).pipe(Effect.flatMap(whoIs), Effect.catchTags({
35723
+ InvalidDpopProof: refused,
35724
+ KeychainError: unsigned
35725
+ }));
34889
35726
  const connected = checkOf("connected", "yes", "this machine holds its grant");
34890
35727
  switch (asked.kind) {
34891
35728
  case "accepted": return [connected, checkOf("accepted", "yes", "ref0 accepts its grant")];
34892
35729
  case "refused": return [connected, checkOf("accepted", "no", "ref0 refuses its grant", yield* connectLineFor(grant.harness, configFolder))];
34893
35730
  case "unanswered": return [connected, checkOf("accepted", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status")];
35731
+ case "unsigned": return [connected, checkOf("accepted", "unknown", "this machine’s key couldn’t sign to ask", asked.nextStep)];
34894
35732
  }
34895
35733
  });
34896
35734
  /** Protocol mismatches either update this CLI or reinstall this folder's plugin. */
@@ -34982,11 +35820,12 @@ const sessionCheck = Effect.fnUntraced(function* (harness, configFolder) {
34982
35820
  const token = yield* Effect.result((yield* Keychain).get(sessionAccount(binding.value)));
34983
35821
  if (Result.isFailure(token)) return checkOf("session", "unknown", `The keychain didn’t say whether ref0 follows this ${configFolders[harness].name} session.`, token.failure.nextStep);
34984
35822
  if (Option.isNone(token.success)) return checkOf("session", "no", "start a new session in this folder", launch);
34985
- const asked = yield* whoIs(token.success.value);
35823
+ const asked = yield* Effect.flatMap(sessionClient(token.success.value), whoIs);
34986
35824
  switch (asked.kind) {
34987
35825
  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}`);
34988
35826
  case "refused": return checkOf("session", "no", "ref0 no longer follows this session", launch);
34989
- case "unanswered": return checkOf("session", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status");
35827
+ case "unanswered":
35828
+ case "unsigned": return checkOf("session", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status");
34990
35829
  }
34991
35830
  });
34992
35831
  const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ json: asJson }) => Effect.gen(function* () {
@@ -35090,9 +35929,19 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
35090
35929
  Ref0Config.layer,
35091
35930
  sessionToken,
35092
35931
  FetchHttpClient.layer
35093
- ])), 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));
35932
+ ])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, MachineKey.layer.pipe(Layer.provide(keychain)), 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([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));
35933
+ //#endregion
35934
+ //#region src/root.ts
35935
+ /**
35936
+ * The CLI's root, around every command and the services it's given. A request's error holds the
35937
+ * request it was sent with, and a request a key signed holds its proof, which is good for a
35938
+ * minute, so `dpop` is redacted wherever one is printed, as `authorization` is. A failure no
35939
+ * command said, a service that couldn't be built included, is logged here, inside that redaction,
35940
+ * rather than by `runMain`, which logs outside it.
35941
+ */
35942
+ 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"])));
35094
35943
  //#endregion
35095
35944
  //#region src/main.ts
35096
- ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);
35945
+ ref0.pipe(Command.run({ version: cliVersion }), atRoot(services), NodeRuntime.runMain({ disableErrorReporting: true }));
35097
35946
  //#endregion
35098
35947
  export {};