@ref0/cli 0.12.1 → 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 +1424 -584
  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,
@@ -6740,6 +7049,7 @@ Schema.Struct({
6740
7049
  request: ApprovalId,
6741
7050
  machine: MachineId,
6742
7051
  machineName: MachineName,
7052
+ cnf: Schema.optionalKey(KeyBinding),
6743
7053
  browserSession: TokenId,
6744
7054
  personGeneration: Generation,
6745
7055
  folders: Schema.Array(ConnectFolderCommand).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
@@ -10345,45 +10655,6 @@ Schema.Union([Schema.Struct({
10345
10655
  found: Schema.Literal(true),
10346
10656
  thread: ThreadId
10347
10657
  }), Schema.Struct({ found: Schema.Literal(false) })]);
10348
- /**
10349
- * ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
10350
- * credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
10351
- * such as `https://1.2.3.999`, `https://[1::2::3]` or `https://ref0.test:99999`, so the platform's own URL parser must take
10352
- * it too. It's kept as written, case and any default port too: every path here is appended to it
10353
- * as it is.
10354
- */
10355
- const ApiOrigin = Schema.String.check(Schema.isPattern(/^https?:\/\/(?:\[[0-9A-Fa-f:.]+\]|[^\s/?#\\@:[\]]+)(?::\d+)?$/), Schema.makeFilter((origin) => URL.canParse(origin) || "a URL the platform can parse")).pipe(Schema.brand("ApiOrigin"));
10356
- /** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
10357
- const signInBase = "/auth";
10358
- /**
10359
- * A route under `signInBase`, by its method: its `path` from the origin's root, as a request
10360
- * names it, and its `pathUnderBase`, as Better Auth names its endpoints.
10361
- */
10362
- const signInRoute = (method, pathUnderBase) => ({
10363
- method,
10364
- pathUnderBase,
10365
- path: `${signInBase}${pathUnderBase}`
10366
- });
10367
- /** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
10368
- const signInRoutes = {
10369
- gitHubSignIn: signInRoute("POST", "/sign-in/social"),
10370
- gitHubCallback: signInRoute("GET", "/callback/github"),
10371
- signOut: signInRoute("POST", "/sign-out")
10372
- };
10373
- /**
10374
- * The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
10375
- * token endpoint, registering a public client and revoking a token.
10376
- */
10377
- const oauthRoutes = {
10378
- metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
10379
- authorize: signInRoute("GET", "/oauth2/authorize"),
10380
- token: signInRoute("POST", "/oauth2/token"),
10381
- register: signInRoute("POST", "/oauth2/register"),
10382
- revoke: signInRoute("POST", "/oauth2/revoke")
10383
- };
10384
- signInRoute("POST", "/oauth2/consent");
10385
- [...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
10386
- `${oauthRoutes.metadata.pathUnderBase}`;
10387
10658
  //#endregion
10388
10659
  //#region ../contracts/src/live-calls.ts
10389
10660
  /**
@@ -11032,22 +11303,75 @@ const TurnTokens = Schema.Union([Schema.Struct({
11032
11303
  known: Schema.Literal("read"),
11033
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"))
11034
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") })]);
11324
+ /**
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
+ });
11035
11355
  /**
11036
- * A turn's facts but its tokens, which `TurnFacts` adds for whoever sends them and `turn.ended`
11037
- * carries only to those who may see them.
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.
11038
11358
  */
11039
11359
  const turnFactsFields = {
11040
11360
  /** The turn's model, where the harness said it: Codex in every hook. */
11041
- model: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 128))),
11361
+ model: Schema.optionalKey(Name),
11042
11362
  modelSource: ModelSource,
11043
11363
  /** Tool calls by tool name. */
11044
- tools: Schema.Record(Schema.String.check(Schema.isBetweenLength(1, 128)), Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
11364
+ tools: Schema.Record(Name, Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
11045
11365
  /** The files its edits touched, by repo and path, at most 50; `more` counts the rest. */
11046
11366
  addresses: Schema.Array(Schema.Struct({
11047
11367
  repo: RepoName,
11048
11368
  path: RepoPath
11049
11369
  })).check(Schema.isMaxLength(50)),
11050
- 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))
11051
11375
  };
11052
11376
  /**
11053
11377
  * What `ref0 mcp` saw of a turn, sent at its `Stop`s and when the next one starts, and again as
@@ -11059,16 +11383,23 @@ const turnFactsFields = {
11059
11383
  const TurnFacts = Schema.Struct({
11060
11384
  ...turnFactsFields,
11061
11385
  tokens: TurnTokens,
11062
- 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)
11063
11389
  });
11064
11390
  /**
11065
- * A turn's facts as `turn.ended` shows them: its tokens only to an admin of the project's owner
11066
- * and to the session's accountable person. A thread's totals still go to every reader of it, in
11067
- * `ThreadUsage`, so on a thread with one person's sessions they are that person's usage.
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.
11068
11395
  */
11069
11396
  const EndedTurnFacts = Schema.Struct({
11070
11397
  ...turnFactsFields,
11071
- 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))
11072
11403
  });
11073
11404
  /**
11074
11405
  * `POST /v1/sessions/{session}/turns`: a new turn, for the harness's own turn ID, so a retry, or
@@ -11696,151 +12027,6 @@ Schema.Struct({
11696
12027
  name: RepoName
11697
12028
  }))
11698
12029
  });
11699
- /** Where an agent's session asks for an identity token. */
11700
- const identityTokenPath = "/v1/me/identity-token";
11701
- /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
11702
- const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
11703
- /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
11704
- const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
11705
- /**
11706
- * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
11707
- * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
11708
- * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
11709
- */
11710
- const parsesAsWritten = (audience) => {
11711
- const parts = audiencePattern.exec(audience);
11712
- if (parts === null || !URL.canParse(audience)) return false;
11713
- const url = new URL(audience);
11714
- const port = parts[2] ?? "";
11715
- return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
11716
- };
11717
- /**
11718
- * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
11719
- * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
11720
- * case and any port too, so `https://verifier.example:443` is another audience than
11721
- * `https://verifier.example`. ref0 never fetches or resolves it.
11722
- */
11723
- const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
11724
- /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
11725
- const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
11726
- /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
11727
- const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
11728
- /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
11729
- const IssuedIdentityToken = Schema.Struct({
11730
- token: IdentityToken,
11731
- expiresAt: Schema.DateTimeUtcFromMillis
11732
- });
11733
- /**
11734
- * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
11735
- * token, naming its issuance in ref0's log without naming any row or secret.
11736
- */
11737
- const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
11738
- /**
11739
- * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
11740
- * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
11741
- * audience, different at every other, and carrying no time.
11742
- */
11743
- const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
11744
- /** A session, as one audience knows it, made as a person's is. */
11745
- const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
11746
- /** The accountable person, last in a token's chain: only their pairwise ID. */
11747
- const PersonActor = Schema.Struct({ sub: PairwisePersonId });
11748
- /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
11749
- const sessionActing = (act) => Schema.Struct({
11750
- sub: PairwiseSessionId,
11751
- act
11752
- });
11753
- const oneSession = sessionActing(PersonActor);
11754
- const twoSessions = sessionActing(oneSession);
11755
- const threeSessions = sessionActing(twoSessions);
11756
- /**
11757
- * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
11758
- * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
11759
- * allows the agent in `sub`, never an actor.
11760
- */
11761
- const IdentityActor = Schema.Union([
11762
- oneSession,
11763
- twoSessions,
11764
- threeSessions,
11765
- sessionActing(threeSessions)
11766
- ]);
11767
- /** A time in a token, in whole UTC epoch seconds. */
11768
- const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
11769
- Schema.Struct({
11770
- iss: ApiOrigin,
11771
- sub: AgentId,
11772
- aud: IdentityAudience,
11773
- iat: EpochSeconds,
11774
- exp: EpochSeconds,
11775
- jti: IdentityTokenId,
11776
- act: IdentityActor
11777
- }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
11778
- /**
11779
- * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
11780
- * never names two keys.
11781
- */
11782
- const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
11783
- Schema.Struct({
11784
- alg: Schema.Literal("ES256"),
11785
- kid: SigningKeyId,
11786
- typ: Schema.Literal("JWT")
11787
- });
11788
- /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
11789
- const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
11790
- /** One public key in the JWKS: exactly these fields, and never a private one. */
11791
- const PublicSigningKey = Schema.Struct({
11792
- kty: Schema.Literal("EC"),
11793
- crv: Schema.Literal("P-256"),
11794
- x: Coordinate,
11795
- y: Coordinate,
11796
- use: Schema.Literal("sig"),
11797
- alg: Schema.Literal("ES256"),
11798
- kid: SigningKeyId
11799
- });
11800
- Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
11801
- Schema.Struct({
11802
- issuer: ApiOrigin,
11803
- jwks_uri: Schema.String,
11804
- subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
11805
- id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
11806
- claims_supported: Schema.Tuple([
11807
- Schema.Literal("iss"),
11808
- Schema.Literal("sub"),
11809
- Schema.Literal("aud"),
11810
- Schema.Literal("exp"),
11811
- Schema.Literal("iat"),
11812
- Schema.Literal("jti"),
11813
- Schema.Literal("act")
11814
- ])
11815
- });
11816
- /**
11817
- * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
11818
- * `IdentityAudience` says. The refused text is never echoed.
11819
- */
11820
- var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
11821
- /**
11822
- * Less than a whole second is left before the session, a parent or the credential expires, or
11823
- * the token would expire before it's recorded: the next prompt renews or replaces the session.
11824
- */
11825
- var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
11826
- /**
11827
- * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
11828
- * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
11829
- * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
11830
- * received or used it. A failed append discards the token, so every token has its event.
11831
- */
11832
- const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
11833
- session: SessionId,
11834
- agent: AgentRef,
11835
- /** The person whose attachment authorized the session's chain, last in the token's `act`. */
11836
- accountable: PersonId,
11837
- jti: IdentityTokenId,
11838
- audience: IdentityAudience,
11839
- issuer: ApiOrigin,
11840
- keyId: SigningKeyId,
11841
- issuedAt: Schema.DateTimeUtcFromMillis,
11842
- expiresAt: Schema.DateTimeUtcFromMillis
11843
- }));
11844
12030
  Schema.Literals([
11845
12031
  "info/refs",
11846
12032
  "git-upload-pack",
@@ -11868,6 +12054,11 @@ Schema.Union([
11868
12054
  Schema.Struct({
11869
12055
  reader: Reader,
11870
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) })),
11871
12062
  push: Schema.optionalKey(Schema.Struct({
11872
12063
  thread: ThreadId,
11873
12064
  session: SessionId
@@ -11881,7 +12072,9 @@ const GitTarget = Schema.Struct({
11881
12072
  * their sessions' forks, and the repo's `main`. Unset for one that reads the repo's project as
11882
12073
  * a whole.
11883
12074
  */
11884
- 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)
11885
12078
  });
11886
12079
  Schema.Union([GitTarget, RepoNotFound]);
11887
12080
  /** A repo whose baseline isn't readable yet: Git is told to try again in a few seconds. */
@@ -11892,28 +12085,6 @@ const CurrentBaseline = Schema.Struct({
11892
12085
  name: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/))
11893
12086
  });
11894
12087
  Schema.Union([CurrentBaseline, RepoNotReady]);
11895
- //#endregion
11896
- //#region ../contracts/src/fork-calls.ts
11897
- /**
11898
- * A session's fork of a repo: its own copy, made in the background from the repo's current
11899
- * baseline when the session first reaches the repo. Agents push only to their own session's
11900
- * forks. A fork isn't deleted yet: when it may be, and what's saved first, waits for the user.
11901
- */
11902
- /**
11903
- * Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
11904
- * project's visibility, which a session's routes read who sees the project by.
11905
- */
11906
- const SessionPlace = Schema.Struct({
11907
- owner: OwnerId,
11908
- project: ProjectId,
11909
- visibility: Visibility,
11910
- thread: ThreadId,
11911
- threadNumber: ThreadNumber,
11912
- session: SessionId,
11913
- number: SessionNumber,
11914
- /** Whether it's still going: not ended, and its lease not run out. */
11915
- live: Schema.Boolean
11916
- });
11917
12088
  Schema.Struct({ session: SessionId });
11918
12089
  Schema.Union([SessionPlace, SessionNotFound]);
11919
12090
  Schema.Struct({
@@ -15214,17 +15385,21 @@ const browserSession = HttpApiSecurity.apiKey({
15214
15385
  key: browserSessionCookie
15215
15386
  });
15216
15387
  /**
15217
- * Every endpoint's credential, checked before its handler runs: a bearer token, or a browser
15218
- * session's cookie. A request with both is refused before it gets here.
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.
15219
15392
  */
15220
15393
  var Authorization = class extends HttpApiMiddleware.Service()("ref0/contracts/Authorization", {
15221
15394
  requiredForClient: true,
15222
15395
  security: {
15223
15396
  bearer: HttpApiSecurity.bearer,
15397
+ dpop: HttpApiSecurity.http({ scheme: "DPoP" }),
15224
15398
  browserSession
15225
15399
  },
15226
15400
  error: [
15227
15401
  Unauthenticated,
15402
+ InvalidDpopProof,
15228
15403
  SessionHasEnded,
15229
15404
  AuthorityUnavailable
15230
15405
  ]
@@ -15255,6 +15430,18 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
15255
15430
  * what to change. Any other part that fails its contract is answered with Effect's empty 400.
15256
15431
  */
15257
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
+ }) {};
15258
15445
  /**
15259
15446
  * An identity token's request whose audience its contract refuses, missing or not an HTTPS
15260
15447
  * origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
@@ -15370,8 +15557,9 @@ var AttachmentsGroup = class extends HttpApiGroup.make("attachments").add(HttpAp
15370
15557
  })).middleware(Authorization) {};
15371
15558
  /**
15372
15559
  * Connecting a machine, the machine's side: asking, with no credential, to connect to one owner's
15373
- * agent, whose terminal shows the code their person types; then collecting what came of it,
15374
- * confirming it saved the grant, or aborting, with its collecting secret.
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.
15375
15563
  */
15376
15564
  var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachments").add(HttpApiEndpoint.post("request", "/v1/attachments/requests", {
15377
15565
  headers: idempotency,
@@ -15381,9 +15569,10 @@ var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachment
15381
15569
  NotPermitted,
15382
15570
  CommandRefused,
15383
15571
  SlowDown,
15572
+ InvalidDpopProof,
15384
15573
  AuthorityUnavailable
15385
15574
  ]
15386
- }).middleware(MachineNameRefusal), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15575
+ }).middleware(MachineNameRefusal).middleware(KeyProof), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15387
15576
  params: { approval: ApprovalId },
15388
15577
  payload: Schema.Struct({ answerCode: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(64))) }),
15389
15578
  success: RequestState,
@@ -18857,6 +19046,8 @@ const HookArguments = Schema.Struct({
18857
19046
  tool_input: ToolValue,
18858
19047
  tool_response: ToolValue,
18859
19048
  error: Field,
19049
+ /** How long the tool ran, in milliseconds, at Claude Code's `PostToolUse` and its failure. */
19050
+ duration_ms: Field,
18860
19051
  /** A string from Claude Code, a boolean from Codex, which keeps a placeholder's JSON type. */
18861
19052
  stop_hook_active: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.String])),
18862
19053
  /** Codex names the turn's model in every hook but `SessionEnd`. */
@@ -19001,12 +19192,129 @@ const projectPage = (page, project) => page.replace("/$owner/$project", `/${proj
19001
19192
  * credential it sends, a bearer token or a browser session.
19002
19193
  */
19003
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));
19004
19197
  /** A bearer token: an agent's credential, sent with every request. */
19005
- const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19198
+ const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19006
19199
  /** No credential: a terminal asking its person, which sends nothing of theirs. */
19007
- const anonymousCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
19200
+ const anonymousCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
19008
19201
  FetchHttpClient.layer;
19009
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
19010
19318
  //#region ../sdk/src/client/request-id.ts
19011
19319
  const decodeRequestId$1 = Schema.decodeUnknownEffect(RequestId);
19012
19320
  /**
@@ -19374,7 +19682,7 @@ const refusedBy = (status, refusal, change) => new CallFailed({
19374
19682
  * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
19375
19683
  * the CLI made. A change it answered may have run.
19376
19684
  */
19377
- const unreadable = (endpoint, change) => new CallFailed({
19685
+ const unreadable$1 = (endpoint, change) => new CallFailed({
19378
19686
  status: 0,
19379
19687
  kind: "Unexpected",
19380
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.`
@@ -19423,7 +19731,7 @@ const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, e
19423
19731
  const state = answered.value;
19424
19732
  heardAt = now;
19425
19733
  if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
19426
- onNone: () => Effect.fail(unreadable(endpoint, change)),
19734
+ onNone: () => Effect.fail(unreadable$1(endpoint, change)),
19427
19735
  onSome: Result.match({
19428
19736
  onSuccess: () => Effect.succeed(state.result),
19429
19737
  onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
@@ -19540,7 +19848,7 @@ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19540
19848
  onNone: () => asPerson(request.command, change, command.endpoint),
19541
19849
  onSome: (bearer) => asSession(bearer, request, command.endpoint)
19542
19850
  });
19543
- 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);
19544
19852
  return answer;
19545
19853
  });
19546
19854
  return Ref0.of({ call });
@@ -19555,7 +19863,7 @@ const callEndpoint = (command) => Effect.gen(function* () {
19555
19863
  return {
19556
19864
  json,
19557
19865
  value: yield* Option.match(successOf(command.endpoint, json), {
19558
- onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19866
+ onNone: () => Effect.fail(unreadable$1(command.endpoint, false)),
19559
19867
  onSome: Effect.succeed
19560
19868
  })
19561
19869
  };
@@ -21340,18 +21648,182 @@ var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
21340
21648
  static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
21341
21649
  };
21342
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
21343
21812
  //#region src/host-grant.ts
21344
21813
  /**
21345
21814
  * All lasting authority for one config folder, stored as one keychain value: the agent the folder
21346
- * acts as, by its owner, handle and agent ID, and its token. The ID is kept in the same write as
21347
- * the token, so a folder with a grant always knows which agent its commits name.
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.
21348
21818
  */
21349
21819
  const KeptHostGrant = Schema.Struct({
21350
21820
  harness: ConnectHarness,
21351
21821
  owner: OwnerName,
21352
21822
  agent: AgentHandle,
21353
21823
  agentId: AgentId,
21354
- 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)
21355
21827
  });
21356
21828
  const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
21357
21829
  const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
@@ -21395,6 +21867,24 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21395
21867
  }
21396
21868
  return Option.none();
21397
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
+ });
21398
21888
  //#endregion
21399
21889
  //#region src/version.ts
21400
21890
  /**
@@ -21403,7 +21893,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21403
21893
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21404
21894
  * and only as a default export.
21405
21895
  */
21406
- const cliVersion = "0.12.1";
21896
+ const cliVersion = "0.13.0";
21407
21897
  //#endregion
21408
21898
  //#region src/harness/hook-protocol.ts
21409
21899
  /**
@@ -21653,6 +22143,7 @@ const yesOrNoQuestions = Effect.gen(function* () {
21653
22143
  const isAnswerCode = Schema.is(TypedApprovalCode);
21654
22144
  const decodeTokenId$1 = Schema.decodeUnknownOption(TokenId);
21655
22145
  const decodeSecretHash$1 = Schema.decodeUnknownEffect(SecretHash);
22146
+ const decodeProofOrigin = Schema.decodeUnknownEffect(ProofOrigin);
21656
22147
  /** How often the machine asks whether its person has answered. */
21657
22148
  const pollMillis = 2e3;
21658
22149
  /** How long one request may take before the machine gives up on it, and asks again. */
@@ -21705,12 +22196,14 @@ const endedWords$1 = (state) => {
21705
22196
  /** The grant a kept record holds, by its ID. */
21706
22197
  const grantOf = (token) => Option.flatMap(parseToken(Redacted.value(token)), (parts) => decodeTokenId$1(parts.token));
21707
22198
  /**
21708
- * Asks with no owner for every folder given, each with a grant secret of its own, shows the
21709
- * link, and takes the answer from the person's own page. Then it keeps each folder ref0
21710
- * connected, confirms exactly the grants it kept, and forgets 0.5.0's grants they replace.
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.
21711
22203
  */
21712
- const requestAttachment = Effect.fnUntraced(function* (asked) {
22204
+ const requestAttachment = Effect.fnUntraced(function* (asked, key) {
21713
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.`)));
21714
22207
  const machine = yield* Machine;
21715
22208
  const hostGrant = yield* HostGrant;
21716
22209
  const oldGrants = yield* OldHostGrants;
@@ -21723,7 +22216,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21723
22216
  const opener = yield* browserOpener;
21724
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)));
21725
22218
  const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
21726
- const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
22219
+ const client = yield* provingClient({
22220
+ origin,
22221
+ store: key.store
22222
+ }).pipe(Effect.provideService(HttpClient.HttpClient, http));
21727
22223
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
21728
22224
  const requestId = yield* newRequestId;
21729
22225
  const asking = yield* Effect.forEach(asked, Effect.fnUntraced(function* (folder) {
@@ -21750,6 +22246,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21750
22246
  const payload = {
21751
22247
  machine: machineId,
21752
22248
  machineName: machineNameOf(machine.hostName),
22249
+ publicKey: key.store.publicKey,
21753
22250
  folders,
21754
22251
  collectHash: yield* Effect.orDie(decodeSecretHash$1(collecting.hash)),
21755
22252
  linkHash: yield* Effect.orDie(decodeSecretHash$1(link.hash))
@@ -21765,6 +22262,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21765
22262
  NotPermitted: said,
21766
22263
  CommandRefused: said,
21767
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.`),
21768
22267
  AuthorityUnavailable: unanswered,
21769
22268
  HttpClientError: unanswered,
21770
22269
  SchemaError: unanswered
@@ -21836,7 +22335,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21836
22335
  owner: connection.owner,
21837
22336
  agent: connection.handle,
21838
22337
  agentId: connection.agent.id,
21839
- 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
21840
22340
  }, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21841
22341
  const finish = Effect.fnUntraced(function* (state) {
21842
22342
  if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
@@ -24637,14 +25137,17 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
24637
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.");
24638
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.");
24639
25139
  yield* (yield* ConnectLock).hold;
25140
+ const machineKey = yield* MachineKey;
25141
+ yield* machineKey.supported;
24640
25142
  yield* (yield* Keychain).probe;
25143
+ const key = yield* machineKey.ensure;
24641
25144
  const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
24642
25145
  const found = yield* findConfigFolders;
24643
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.`);
24644
25147
  const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
24645
25148
  if (chosen.length === 0) return yield* sayFailed(`No config folder was ticked, so nothing was asked. To connect one, run ${again} again.`);
24646
25149
  yield* Console.error("");
24647
- const connected = yield* requestAttachment(chosen);
25150
+ const connected = yield* requestAttachment(chosen, key);
24648
25151
  const installed = yield* installInto(connected.kept, { asJson });
24649
25152
  const others = yield* otherOwners(connected);
24650
25153
  yield* printAnswer(answerOf(connected, installed, others), asJson, reportOf(connected, installed, others, home));
@@ -25210,9 +25713,9 @@ const signalExits = {
25210
25713
  };
25211
25714
  const decodeSetup = Schema.decodeUnknownOption(Schema.Array(Schema.String));
25212
25715
  /** A duration's milliseconds, as a check writes it, `'30 seconds'`, or a number of them. */
25213
- const millisOf = (duration) => Option.map(Duration.fromInput(duration), (read) => Math.ceil(Duration.toMillis(read)));
25716
+ const millisOf$1 = (duration) => Option.map(Duration.fromInput(duration), (read) => Math.ceil(Duration.toMillis(read)));
25214
25717
  /** A duration a command was given, in milliseconds, or why it isn't one. */
25215
- const givenMillis = (duration) => Option.match(millisOf(duration), {
25718
+ const givenMillis = (duration) => Option.match(millisOf$1(duration), {
25216
25719
  onNone: () => Effect.fail(new LocalSandboxFailed({ why: `Not a duration: ${duration}` })),
25217
25720
  onSome: Effect.succeed
25218
25721
  });
@@ -25555,7 +26058,7 @@ const defaultRetries = {
25555
26058
  };
25556
26059
  const defaultTimeout = 6e5;
25557
26060
  /** A duration as a step's config writes it, in milliseconds, or the run's code's mistake. */
25558
- const stepMillis = (duration) => Option.match(millisOf(duration), {
26061
+ const stepMillis = (duration) => Option.match(millisOf$1(duration), {
25559
26062
  onNone: () => Effect.die(/* @__PURE__ */ new Error(`Not a duration: ${duration}`)),
25560
26063
  onSome: Effect.succeed
25561
26064
  });
@@ -26875,7 +27378,7 @@ const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
26875
27378
  * or the thread below, set apart.
26876
27379
  */
26877
27380
  const baseNamed = (on, main = "main") => on === "main" ? main : `#${on.thread}, which this thread is stacked on,`;
26878
- const decodeWritten$1 = Schema.decodeUnknownOption(WrittenRepo);
27381
+ const decodeWritten$2 = Schema.decodeUnknownOption(WrittenRepo);
26879
27382
  const decodeCommit$1 = Schema.decodeUnknownOption(CommitOid);
26880
27383
  const refused$1 = (why) => ({
26881
27384
  kind: "refused",
@@ -26909,7 +27412,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
26909
27412
  stdout: "inherit",
26910
27413
  stderr: "inherit"
26911
27414
  })).pipe(Effect.map(Number), Effect.catchCause(() => Effect.succeed(-1)));
26912
- const written = decodeWritten$1((yield* read([
27415
+ const written = decodeWritten$2((yield* read([
26913
27416
  "config",
26914
27417
  "--get",
26915
27418
  "ref0.repo"
@@ -30075,7 +30578,7 @@ const watchCommands = [Command.make("watch", {
30075
30578
  owner: ownerName,
30076
30579
  agent: flags.agent
30077
30580
  });
30078
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)));
30581
+ const client = yield* grantClient(grant.value);
30079
30582
  const watching = {
30080
30583
  project: flags.project,
30081
30584
  parent: Option.getOrUndefined(flags.parent),
@@ -30104,7 +30607,7 @@ const watchCommands = [Command.make("watch", {
30104
30607
  parent: number
30105
30608
  })
30106
30609
  })
30107
- }).pipe(Effect.mapError((error) => Match.value(error).pipe(Match.tag("Unauthenticated", "SessionHasEnded", () => stopped(`ref0 refused the host grant ref0 connect kept for ${as} on this machine: it was revoked or never confirmed, or the machine was disconnected. Connect it again with ref0 connect, then run ref0 watch again.`)), Match.tag("ProjectNotFound", () => stopped(`${flags.project} isn’t a project ${as} can read. Check the owner/project, and that ${flags.agent} is the agent to watch as.`)), Match.tag("ThreadNotFound", () => stopped(`${flags.project} has no thread #${Option.getOrElse(flags.parent, () => 0)} to watch the children of. Find its number with ref0 threads ${flags.project}.`)), Match.tag("NotPermitted", (refused) => stopped(refused.nextStep)), Match.tag("AuthorityUnavailable", () => new ReadUnavailable({ why: "it’s restarting" })), Match.tag("ProjectNotReady", () => new ReadUnavailable({ why: `${flags.project} isn’t ready yet` })), Match.orElse(() => new ReadUnavailable({ why: `couldn’t reach ${config.api}` })))));
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}` })))));
30108
30611
  return yield* Effect.scoped(Effect.gen(function* () {
30109
30612
  const state = yield* openWatchState(folder, {
30110
30613
  project: flags.project,
@@ -30124,7 +30627,8 @@ const watchCommands = [Command.make("watch", {
30124
30627
  }).pipe(Effect.catchTags({
30125
30628
  WatchStopped: (stopped) => sayFailed(stopped.nextStep),
30126
30629
  WatchStateError: (failed) => sayFailed(failed.nextStep),
30127
- KeychainError: (failed) => sayFailed(failed.nextStep)
30630
+ KeychainError: (failed) => sayFailed(failed.nextStep),
30631
+ InvalidDpopProof: (refused) => sayFailed(refused.nextStep)
30128
30632
  }))).pipe(Command.withShortDescription("Run a command for each thread started in a project"), Command.withDescription([
30129
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.",
30130
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.",
@@ -30661,7 +31165,7 @@ function editedPaths(hook) {
30661
31165
  /** The most bytes of a file read around an edit; a larger one's edits aren't attributed. */
30662
31166
  const fileBytes = 2e6;
30663
31167
  /** The most edit calls waiting for their `PostToolUse`; a harness that never sends one leaks none. */
30664
- const openCalls = 64;
31168
+ const openCalls$1 = 64;
30665
31169
  /** The most line hashes a worktree keeps pending; the oldest edits go first. */
30666
31170
  const pendingLines = 2e4;
30667
31171
  /** A line as it's kept pending: 128 bits of its SHA-256, never its text. */
@@ -31030,7 +31534,7 @@ var Attribution = class Attribution extends Context.Service()("ref0/cli/Attribut
31030
31534
  turn,
31031
31535
  files
31032
31536
  });
31033
- 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);
31034
31538
  return {
31035
31539
  ...latest,
31036
31540
  calls
@@ -31278,7 +31782,7 @@ const optionsWithValues = /* @__PURE__ */ new Set([
31278
31782
  "--git-dir",
31279
31783
  "--work-tree"
31280
31784
  ]);
31281
- const decodeWritten = Schema.decodeUnknownOption(WrittenRepo);
31785
+ const decodeWritten$1 = Schema.decodeUnknownOption(WrittenRepo);
31282
31786
  const hunkHeader = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
31283
31787
  /** The command a Git run's arguments name, past Git's own options. */
31284
31788
  const commandOf = (args) => {
@@ -31626,7 +32130,7 @@ const checkBase = Effect.fnUntraced(function* (folder, base, delivered, within)
31626
32130
  "--get",
31627
32131
  "ref0.repo"
31628
32132
  ]);
31629
- const repo = decodeWritten(named.out.trim());
32133
+ const repo = decodeWritten$1(named.out.trim());
31630
32134
  if (Option.isNone(repo)) return yield* failed("this checkout has no ref0.repo to load history for");
31631
32135
  yield* loadSyncHistory(store, repo.value).pipe(Effect.mapError((error) => failed(error.why)));
31632
32136
  yield* fs.writeFileString(loaded, "");
@@ -32384,6 +32888,321 @@ var HarnessTasks = class HarnessTasks extends Context.Service()("ref0/cli/Harnes
32384
32888
  }));
32385
32889
  };
32386
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
32387
33206
  //#region src/harness/rollouts.ts
32388
33207
  /**
32389
33208
  * Codex's rollouts, as `ref0 mcp` reads a turn's tokens from them: each hook names the
@@ -32403,7 +33222,8 @@ var HarnessTasks = class HarnessTasks extends Context.Service()("ref0/cli/Harnes
32403
33222
  * `token_count`s, then the subagent's own turn, its total from zero, a forked subagent's as a
32404
33223
  * fresh one's (Codex 0.163.0's subagents, 20 Oct 2026). So its requests are those past its own
32405
33224
  * turn's `task_started`, and what its total carried over, nothing so far, is its first request's
32406
- * total less that request's own usage.
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).
32407
33227
  */
32408
33228
  const Count$1 = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
32409
33229
  /** A session's running total, as far as ref0 counts it: reasoning is inside output. */
@@ -32451,12 +33271,16 @@ const zero = {
32451
33271
  cacheWrite: 0,
32452
33272
  output: 0
32453
33273
  };
32454
- /** 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
+ */
32455
33278
  const counted = [
32456
33279
  "\"token_count\"",
32457
33280
  "\"turn_context\"",
32458
33281
  "\"task_started\"",
32459
- "\"turn_aborted\""
33282
+ "\"turn_aborted\"",
33283
+ "\"compacted\""
32460
33284
  ];
32461
33285
  /** A line, as far as telling Codex's records apart, and the turn an event is of. */
32462
33286
  const Line$1 = Schema.Struct({
@@ -32541,6 +33365,7 @@ const tokensOf = (model, step) => ({
32541
33365
  */
32542
33366
  function stepsIn(text, from, until) {
32543
33367
  const steps = [];
33368
+ const compactions = [];
32544
33369
  const lines = text.split("\n");
32545
33370
  let { top, total, model } = from;
32546
33371
  let whole = true;
@@ -32557,6 +33382,7 @@ function stepsIn(text, from, until) {
32557
33382
  const event = kind.value.type === "event_msg" ? kind.value.payload : void 0;
32558
33383
  if (event?.type === "task_started" && until !== void 0 && event.turn_id === until) return {
32559
33384
  steps,
33385
+ compactions,
32560
33386
  running: {
32561
33387
  top,
32562
33388
  total,
@@ -32565,6 +33391,11 @@ function stepsIn(text, from, until) {
32565
33391
  whole,
32566
33392
  before: index
32567
33393
  };
33394
+ if (kind.value.type === "compacted") {
33395
+ const at = writtenAt(line);
33396
+ compactions.push(at === void 0 ? {} : { at });
33397
+ continue;
33398
+ }
32568
33399
  if (event?.type === "turn_aborted") {
32569
33400
  whole = false;
32570
33401
  continue;
@@ -32595,6 +33426,7 @@ function stepsIn(text, from, until) {
32595
33426
  }
32596
33427
  return {
32597
33428
  steps,
33429
+ compactions,
32598
33430
  running: {
32599
33431
  top,
32600
33432
  total,
@@ -32612,7 +33444,9 @@ function stepsIn(text, from, until) {
32612
33444
  * Each holds one JSON object a line, and writes an assistant reply over several lines, each with
32613
33445
  * the reply's `message.usage` so far: the session's own repeat it, and a subagent's grow its
32614
33446
  * output line by line. So a reply counts once, by its `message.id`, at the most its lines say,
32615
- * with its own model (Claude Code 2.1.304's transcripts, 20 Oct 2026). Codex's: the session's
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
32616
33450
  * rollout, whose running total `rollouts.ts` reads, kept with its place, and each subagent's,
32617
33451
  * which its `SubagentStop` names, read first from where its own turn starts, past the parent's
32618
33452
  * history it copies. Each is read from the
@@ -32638,8 +33472,13 @@ const linesEnd = (bytes, count) => {
32638
33472
  return end;
32639
33473
  };
32640
33474
  const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
32641
- /** A line, as far as telling an assistant's from the rest. */
32642
- const Line = Schema.Struct({ type: Schema.String });
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"]) }) });
32643
33482
  /** An assistant line: a reply's ID, its model and its usage, which every line of it repeats. */
32644
33483
  const AssistantLine = Schema.Struct({ message: Schema.Struct({
32645
33484
  model: Schema.String,
@@ -32653,18 +33492,34 @@ const AssistantLine = Schema.Struct({ message: Schema.Struct({
32653
33492
  }) });
32654
33493
  const decodeLine = Schema.decodeUnknownOption(Schema.fromJsonString(Line));
32655
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
+ };
32656
33505
  /** What Claude Code names the model of a reply it wrote itself, such as an error: no model's. */
32657
33506
  const synthetic = "<synthetic>";
32658
33507
  /**
32659
- * The replies in a transcript's whole lines, in order, or none if an assistant line can't be read,
32660
- * since its tokens would go missing unseen. A line that isn't JSON is passed over: a read that
32661
- * starts inside a line, after a transcript shrank, reads its tail as one.
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.
32662
33511
  */
32663
- function repliesIn(text) {
33512
+ function transcriptIn(text) {
32664
33513
  const replies = [];
33514
+ const compactions = [];
32665
33515
  for (const line of text.split("\n")) {
32666
33516
  const kind = decodeLine(line);
32667
- 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;
32668
33523
  const assistant = decodeAssistant(line);
32669
33524
  if (Option.isNone(assistant)) return Option.none();
32670
33525
  const { model, id, usage } = assistant.value.message;
@@ -32679,8 +33534,17 @@ function repliesIn(text) {
32679
33534
  cacheRead: usage.cache_read_input_tokens ?? 0
32680
33535
  });
32681
33536
  }
32682
- return Option.some(replies);
33537
+ return Option.some({
33538
+ replies,
33539
+ compactions
33540
+ });
32683
33541
  }
33542
+ /** A read that found nothing. */
33543
+ const nothingIn = {
33544
+ replies: [],
33545
+ steps: [],
33546
+ compactions: []
33547
+ };
32684
33548
  /** A transcript never read: its top. */
32685
33549
  const top = {
32686
33550
  offset: 0,
@@ -32713,6 +33577,23 @@ const KeptCounts = Schema.Struct({
32713
33577
  cacheWrite: Count,
32714
33578
  cacheRead: Count
32715
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
+ });
32716
33597
  const KeptFile = Schema.Struct({
32717
33598
  generation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
32718
33599
  read: Schema.Array(Schema.Struct({
@@ -32725,24 +33606,42 @@ const KeptFile = Schema.Struct({
32725
33606
  turn: Schema.optionalKey(Schema.Struct({
32726
33607
  number: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
32727
33608
  facts: TurnFacts,
32728
- known: Schema.Literals([
32729
- "waiting",
32730
- "read",
32731
- "failed"
32732
- ]),
32733
- models: Schema.Array(Schema.Struct({
32734
- model: Schema.String,
32735
- ...KeptCounts.fields
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
32736
33623
  })),
32737
- replies: Schema.Array(Schema.Struct({
32738
- id: Schema.String,
32739
- model: Schema.String,
32740
- ...KeptCounts.fields
32741
- }))
33624
+ moreSubagents: Count
32742
33625
  }))
32743
33626
  });
32744
33627
  const encodeKept = Schema.encodeSync(Schema.fromJsonString(KeptFile));
32745
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
+ });
32746
33645
  const fromFile = (file) => ({
32747
33646
  generation: file.generation,
32748
33647
  places: new Map(file.read.map(({ path, ...place }) => [path, place])),
@@ -32750,10 +33649,19 @@ const fromFile = (file) => ({
32750
33649
  turn: Option.fromNullishOr(file.turn).pipe(Option.map((turn) => ({
32751
33650
  number: turn.number,
32752
33651
  facts: turn.facts,
32753
- tokens: {
32754
- known: turn.known,
32755
- models: new Map(turn.models.map(({ model, ...counts }) => [model, counts])),
32756
- replies: new Map(turn.replies.map(({ id, ...reply }) => [id, reply]))
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
32757
33665
  }
32758
33666
  })))
32759
33667
  });
@@ -32767,20 +33675,32 @@ const toFile = (kept) => {
32767
33675
  own: kept.own
32768
33676
  };
32769
33677
  if (Option.isSome(kept.turn)) {
32770
- const { number, facts, tokens } = kept.turn.value;
32771
- file.turn = {
33678
+ const { number, facts, reads, startedAt } = kept.turn.value;
33679
+ const { known, peak, compactions } = reads.context;
33680
+ const turn = {
32772
33681
  number,
32773
33682
  facts,
32774
- known: tokens.known,
32775
- models: [...tokens.models].map(([model, counts]) => ({
32776
- model,
32777
- ...counts
32778
- })),
32779
- replies: [...tokens.replies].map(([id, reply]) => ({
32780
- id,
32781
- ...reply
32782
- }))
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
32783
33701
  };
33702
+ if (startedAt !== void 0) turn.startedAt = startedAt;
33703
+ file.turn = turn;
32784
33704
  }
32785
33705
  return file;
32786
33706
  };
@@ -32877,8 +33797,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32877
33797
  const readOne = Effect.fnUntraced(function* (harness, transcript, from, until) {
32878
33798
  const info = yield* fs.stat(transcript);
32879
33799
  if (info.type !== "File") return {
32880
- replies: [],
32881
- steps: [],
33800
+ ...nothingIn,
32882
33801
  whole: false,
32883
33802
  to: from,
32884
33803
  reached: false
@@ -32890,6 +33809,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32890
33809
  const lost = harness === "codex" && (!same || from.running === void 0);
32891
33810
  const replies = [];
32892
33811
  const steps = [];
33812
+ const compactions = [];
32893
33813
  let at = lost ? yield* tailOf(file, size) : same ? from.offset : 0;
32894
33814
  let running = lost ? totalLost : from.running;
32895
33815
  let whole = !lost;
@@ -32908,6 +33828,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32908
33828
  if (harness === "codex") {
32909
33829
  const found = stepsIn(text, running ?? totalLost, until);
32910
33830
  steps.push(...found.steps);
33831
+ compactions.push(...found.compactions);
32911
33832
  running = found.running;
32912
33833
  whole &&= found.whole;
32913
33834
  if (found.before !== void 0) {
@@ -32916,8 +33837,11 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32916
33837
  break;
32917
33838
  }
32918
33839
  } else {
32919
- const found = repliesIn(text);
32920
- 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
+ }
32921
33845
  whole &&= Option.isSome(found);
32922
33846
  }
32923
33847
  at += lines;
@@ -32926,6 +33850,7 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32926
33850
  return {
32927
33851
  replies,
32928
33852
  steps,
33853
+ compactions,
32929
33854
  whole,
32930
33855
  to: placeOf(at, hash, running),
32931
33856
  reached
@@ -32978,25 +33903,27 @@ var Transcripts = class Transcripts extends Context.Service()("ref0/cli/Transcri
32978
33903
  const state = yield* stateOf(owner);
32979
33904
  const replies = [];
32980
33905
  const steps = [];
33906
+ const compactions = [];
32981
33907
  const places = /* @__PURE__ */ new Map();
32982
33908
  let whole = true;
32983
33909
  for (const transcript of transcripts) {
32984
33910
  const since = harness === "codex" ? turn?.since : void 0;
32985
33911
  const from = state.places.get(transcript) ?? (since === void 0 ? top : yield* subagentStartOf(transcript, since));
32986
33912
  const one = yield* readOne(harness, transcript, from, turn?.until).pipe(Effect.orElseSucceed(() => ({
32987
- replies: [],
32988
- steps: [],
33913
+ ...nothingIn,
32989
33914
  whole: false,
32990
33915
  to: from
32991
33916
  })));
32992
33917
  replies.push(...one.replies);
32993
33918
  steps.push(...one.steps);
33919
+ compactions.push(...one.compactions);
32994
33920
  places.set(transcript, one.to);
32995
33921
  whole &&= one.whole;
32996
33922
  }
32997
33923
  return {
32998
33924
  replies,
32999
33925
  steps,
33926
+ compactions,
33000
33927
  places,
33001
33928
  whole
33002
33929
  };
@@ -33181,6 +34108,8 @@ function withContext(answer, event, delivered) {
33181
34108
  //#region src/harness/hook-input.ts
33182
34109
  const decodeObjectText = Schema.decodeUnknownOption(Schema.fromJsonString(JsonObject));
33183
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;
33184
34113
  const toolValue = (value) => {
33185
34114
  if (value === void 0 || value === "") return void 0;
33186
34115
  if (value instanceof Object) return value;
@@ -33194,22 +34123,26 @@ function readHook(args) {
33194
34123
  };
33195
34124
  const turn = filled(args.prompt_id) ?? filled(args.turn_id);
33196
34125
  const agent = filled(args.agent_id);
34126
+ const agentType = filled(args.agent_type);
33197
34127
  const name = filled(args.tool_name);
33198
34128
  const callId = filled(args.tool_use_id);
33199
34129
  const input = toolValue(args.tool_input);
33200
34130
  const response = toolValue(args.tool_response);
34131
+ const durationMs = millisOf(filled(args.duration_ms));
33201
34132
  const model = filled(args.model);
33202
34133
  const cwd = filled(args.cwd);
33203
34134
  const transcript = filled(args.transcript_path);
33204
34135
  const agentTranscript = filled(args.agent_transcript_path);
33205
34136
  if (turn !== void 0) hook.harnessTurn = turn;
33206
34137
  if (agent !== void 0) hook.agent = agent;
34138
+ if (agentType !== void 0) hook.agentType = agentType;
33207
34139
  if (name !== void 0) hook.tool = callId === void 0 ? { name } : {
33208
34140
  name,
33209
34141
  callId
33210
34142
  };
33211
34143
  if (input instanceof Object) hook.toolInput = input;
33212
34144
  if (response !== void 0) hook.toolResponse = response;
34145
+ if (durationMs !== void 0) hook.durationMs = durationMs;
33213
34146
  if (model !== void 0) hook.model = model;
33214
34147
  if (cwd !== void 0) hook.cwd = cwd;
33215
34148
  if (transcript !== void 0) hook.transcript = transcript;
@@ -33266,11 +34199,11 @@ const whyNotSaidYet = "Not said yet: the agent says why this thread is needed, a
33266
34199
  const startHarnessSession = Effect.fnUntraced(function* (input) {
33267
34200
  const hostGrant = yield* HostGrant;
33268
34201
  const keychain = yield* Keychain;
33269
- const config = yield* Ref0Config;
33270
34202
  const harness = input.binding.harness === "codex" ? "codex" : "claude-code";
33271
34203
  const folder = yield* (yield* ConfigFolders).folder(harness);
33272
34204
  const grant = yield* hostGrant.get(folder);
33273
34205
  if (Option.isNone(grant) || grant.value.harness !== harness) return Option.none();
34206
+ const client = yield* grantClient(grant.value);
33274
34207
  const pending = pendingAccount(input.binding);
33275
34208
  const held = Option.flatMap(yield* keychain.get(sessionAccount(input.binding)), (kept) => parseToken(Redacted.value(kept)));
33276
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));
@@ -33305,7 +34238,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
33305
34238
  trigger: input.trigger,
33306
34239
  attended: "reported"
33307
34240
  });
33308
- const started = yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)))).sessions.start({
34241
+ const started = yield* client.sessions.start({
33309
34242
  headers: { "idempotency-key": requestId },
33310
34243
  payload
33311
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))));
@@ -33635,175 +34568,53 @@ const sessionTasks = Effect.gen(function* () {
33635
34568
  };
33636
34569
  });
33637
34570
  //#endregion
33638
- //#region src/harness/turn-facts.ts
33639
- /** The most tool names a turn's record counts. */
33640
- const toolNames = 100;
33641
- /** The most replies a turn's tally remembers; a turn with more has its tokens unknown. */
33642
- const turnReplies = 1e4;
33643
- const nothingSeen = {
33644
- tools: /* @__PURE__ */ new Map(),
33645
- addresses: [],
33646
- more: 0,
33647
- model: void 0,
33648
- tokens: {
33649
- known: "waiting",
33650
- models: /* @__PURE__ */ new Map(),
33651
- replies: /* @__PURE__ */ new Map()
33652
- },
33653
- startedAt: void 0,
33654
- stoppedAt: void 0
33655
- };
33656
- /**
33657
- * A hook seen at `at`: a tool call counted by its tool once it ran, the files an edit is about to
33658
- * touch kept, at most 50 and the rest counted, the model, where the hook names one, and a `Stop`
33659
- * as when the agent last stopped.
33660
- */
33661
- function seeHook(seen, hook, edited, at) {
33662
- const tools = new Map(seen.tools);
33663
- const name = hook.tool?.name;
33664
- if (hook.event === "PostToolUse" && name !== void 0) {
33665
- const counted = tools.get(name);
33666
- if (counted !== void 0 || tools.size < toolNames) tools.set(name, (counted ?? 0) + 1);
33667
- }
33668
- const addresses = [...seen.addresses];
33669
- let more = seen.more;
33670
- for (const each of edited) {
33671
- if (addresses.some((kept) => kept.repo === each.repo && kept.path === each.path)) continue;
33672
- if (addresses.length < 50) addresses.push(each);
33673
- else more += 1;
33674
- }
33675
- return {
33676
- ...seen,
33677
- tools,
33678
- addresses,
33679
- more,
33680
- model: hook.model ?? seen.model,
33681
- stoppedAt: hook.event === "Stop" ? at : seen.stoppedAt
33682
- };
33683
- }
33684
- /**
33685
- * A read's replies added to a turn's, each by its message ID at the most any of its lines says:
33686
- * a subagent's transcript writes a reply's output as it grows, line by line, so a line of a reply
33687
- * already counted adds only what it grew. `own` when the read was of the session's own
33688
- * transcript, which makes the turn's tokens known; nothing makes them known once a read has
33689
- * failed.
33690
- */
33691
- function addReplies(tally, replies, own) {
33692
- if (tally.known === "failed") return tally;
33693
- const models = new Map(tally.models);
33694
- const counted = new Map(tally.replies);
33695
- for (const reply of replies) {
33696
- const before = counted.get(reply.id);
33697
- const most = {
33698
- model: before?.model ?? reply.model,
33699
- input: Math.max(before?.input ?? 0, reply.input),
33700
- output: Math.max(before?.output ?? 0, reply.output),
33701
- cacheWrite: Math.max(before?.cacheWrite ?? 0, reply.cacheWrite),
33702
- cacheRead: Math.max(before?.cacheRead ?? 0, reply.cacheRead)
33703
- };
33704
- const sum = models.get(most.model);
33705
- counted.set(reply.id, most);
33706
- models.set(most.model, {
33707
- input: (sum?.input ?? 0) + most.input - (before?.input ?? 0),
33708
- output: (sum?.output ?? 0) + most.output - (before?.output ?? 0),
33709
- cacheWrite: (sum?.cacheWrite ?? 0) + most.cacheWrite - (before?.cacheWrite ?? 0),
33710
- cacheRead: (sum?.cacheRead ?? 0) + most.cacheRead - (before?.cacheRead ?? 0)
33711
- });
33712
- }
33713
- if (counted.size > turnReplies) return readFailed(tally);
33714
- return {
33715
- known: own ? "read" : tally.known,
33716
- models,
33717
- replies: counted
33718
- };
33719
- }
33720
- /**
33721
- * A read's requests added to a turn's, by model, as Codex's rollout steps its running total.
33722
- * `own` as for `addReplies`: a rollout is always the session's own.
33723
- */
33724
- function addSteps(tally, steps, own) {
33725
- if (tally.known === "failed") return tally;
33726
- const models = new Map(tally.models);
33727
- for (const step of steps) {
33728
- const sum = models.get(step.model);
33729
- models.set(step.model, {
33730
- input: (sum?.input ?? 0) + step.input,
33731
- output: (sum?.output ?? 0) + step.output,
33732
- cacheWrite: (sum?.cacheWrite ?? 0) + step.cacheWrite,
33733
- cacheRead: (sum?.cacheRead ?? 0) + step.cacheRead
33734
- });
33735
- }
33736
- return {
33737
- ...tally,
33738
- known: own ? "read" : tally.known,
33739
- models
33740
- };
33741
- }
33742
- /** A read that failed: the turn's tokens are unknown from then on. */
33743
- const readFailed = (tally) => ({
33744
- ...tally,
33745
- known: "failed"
33746
- });
33747
- /** A read's replies and requests added to a turn's tokens, or its tokens unknown if it failed. */
33748
- const addRead = (tally, read, own) => read.whole ? addSteps(addReplies(tally, read.replies, own), read.steps, own) : readFailed(tally);
33749
- /**
33750
- * A turn's tokens as ref0 keeps them: each model's counts once the session's own transcript is
33751
- * read, or unknown, with no counts, where it wasn't, a read failed, or a count passes what a
33752
- * turn may hold.
33753
- */
33754
- function turnTokens(tally) {
33755
- if (tally.known !== "read" || tally.models.size > 16) return { known: "unknown" };
33756
- const models = [...tally.models].map(([model, counts]) => ({
33757
- model,
33758
- ...counts
33759
- }));
33760
- return models.every((each) => Math.max(each.input, each.output, each.cacheWrite, each.cacheRead) <= 1e8 && each.model.length >= 1 && each.model.length <= 128) ? {
33761
- known: "read",
33762
- models
33763
- } : { known: "unknown" };
33764
- }
33765
- /**
33766
- * A turn's facts as ref0 keeps them, with its stop as how long after ref0 started it the agent last
33767
- * stopped: none until both are seen, and 0 for a `Stop` before its start landed.
33768
- */
33769
- function turnFacts(seen) {
33770
- const facts = {
33771
- modelSource: seen.model === void 0 ? "unknown" : "hook",
33772
- tools: Object.fromEntries(seen.tools),
33773
- addresses: seen.addresses,
33774
- more: seen.more,
33775
- tokens: turnTokens(seen.tokens)
33776
- };
33777
- if (seen.model !== void 0) facts.model = seen.model;
33778
- if (seen.startedAt !== void 0 && seen.stoppedAt !== void 0) facts.stoppedAfterMs = Math.max(seen.stoppedAt - seen.startedAt, 0);
33779
- return facts;
33780
- }
33781
- //#endregion
33782
34571
  //#region src/harness/session-turns.ts
33783
34572
  /** The open turn as `SessionEnd` tells it once more: none until ref0 has numbered it. */
33784
34573
  const keptOf = (now) => now.kind === "open" ? Option.some({
33785
34574
  number: now.number,
33786
34575
  facts: turnFacts(now.seen),
33787
- tokens: now.seen.tokens
34576
+ reads: now.seen,
34577
+ startedAt: now.seen.startedAt
33788
34578
  }) : Option.none();
33789
34579
  /**
33790
- * The transcripts a hook's read takes, and whether the session's own is among them: at the
33791
- * tracked turn's own `Stop`, the session's; at any subagent's `SubagentStop`, its own, whichever
33792
- * turn started it, since it adds to the turn still open, with the turn Codex names there, the
33793
- * subagent's own, where its rollout's own part starts. A new turn's prompt reads the session's
33794
- * first, for the turn it ends.
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.
33795
34585
  */
33796
34586
  const toRead = (hook, ownHook) => {
33797
34587
  if (hook.event === "Stop" && ownHook && hook.transcript !== void 0) return {
33798
34588
  transcripts: [hook.transcript],
33799
- own: true
34589
+ of: { own: true }
33800
34590
  };
33801
34591
  if (hook.event === "SubagentStop" && hook.agentTranscript !== void 0) return {
33802
34592
  transcripts: [hook.agentTranscript],
33803
- own: false,
34593
+ of: {
34594
+ own: false,
34595
+ subagent: hook.agentTranscript,
34596
+ type: hook.agentType
34597
+ },
33804
34598
  turn: { since: hook.harnessTurn }
33805
34599
  };
33806
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;
33807
34618
  /** A harness session's turns, each numbered by ref0 as it starts. */
33808
34619
  const sessionTurns = Effect.fnUntraced(function* (edges) {
33809
34620
  const transcripts = yield* Transcripts;
@@ -33824,7 +34635,11 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33824
34635
  const ending = prompted === void 0 ? void 0 : yield* transcripts.read(owner, harness, [prompted], { until: harnessTurn });
33825
34636
  const fresh = late && harness === "claude-code" ? {
33826
34637
  ...nothingSeen,
33827
- tokens: readFailed(nothingSeen.tokens)
34638
+ tokens: readFailed(nothingSeen.tokens),
34639
+ context: {
34640
+ ...nothingSeen.context,
34641
+ known: "failed"
34642
+ }
33828
34643
  } : nothingSeen;
33829
34644
  const tracked = isNew ? Option.some({
33830
34645
  kind: "starting",
@@ -33833,7 +34648,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33833
34648
  turn: now.number,
33834
34649
  seen: ending === void 0 ? now.seen : {
33835
34650
  ...now.seen,
33836
- tokens: addRead(now.seen.tokens, ending, true)
34651
+ ...addRead(now.seen, ending, { own: true })
33837
34652
  }
33838
34653
  }) : now.previous),
33839
34654
  seen: fresh
@@ -33845,9 +34660,13 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33845
34660
  const hooked = ownHook && !isNew ? seeHook(tracked.value.seen, hook, edited, yield* Clock.currentTimeMillis) : tracked.value.seen;
33846
34661
  const reading = toRead(hook, ownHook);
33847
34662
  const read = reading === void 0 ? void 0 : yield* transcripts.read(owner, harness, reading.transcripts, reading.turn);
33848
- const seen = read === void 0 || reading === void 0 ? hooked : {
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
+ } : {
33849
34668
  ...hooked,
33850
- tokens: addRead(hooked.tokens, read, reading.own)
34669
+ ...addRead(hooked, read, reading.of)
33851
34670
  };
33852
34671
  const places = (read ?? ending)?.places;
33853
34672
  let now = {
@@ -33856,7 +34675,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33856
34675
  };
33857
34676
  yield* Effect.uninterruptible(Effect.gen(function* () {
33858
34677
  yield* Ref.set(turn, Option.some(now));
33859
- 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));
33860
34679
  }));
33861
34680
  if (now.kind === "starting") {
33862
34681
  if (Option.isSome(now.previous)) yield* edges.tasksLanded(client, bound, deadline);
@@ -33907,7 +34726,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33907
34726
  if (ownHook && hook.event === "Stop") {
33908
34727
  yield* edges.tasksLanded(client, bound, deadline);
33909
34728
  yield* tellFacts;
33910
- } else if (hook.event === "SubagentStop" && read !== void 0) yield* tellFacts;
34729
+ } else if (hook.event === "SubagentStop" && (read !== void 0 || unseen !== void 0)) yield* tellFacts;
33911
34730
  });
33912
34731
  return {
33913
34732
  now: Ref.get(turn),
@@ -34311,7 +35130,7 @@ const endWithin = "5 seconds";
34311
35130
  const lastReadWithin = "2 seconds";
34312
35131
  /**
34313
35132
  * The turn `ref0 mcp` last told ref0 of, told once more with what the session's transcript
34314
- * gained since: a reply written after its last `Stop` counts in it, and a `Stop` whose facts
35133
+ * gained since: a reply or a compaction written after its last `Stop` counts in it, and a `Stop` whose facts
34315
35134
  * didn't land has them sent again. Nothing for a turn ref0 hasn't numbered.
34316
35135
  */
34317
35136
  const lastRead = Effect.fnUntraced(function* (client, session, harness, transcript) {
@@ -34324,7 +35143,7 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
34324
35143
  };
34325
35144
  const turn = kept.value.turn.value;
34326
35145
  const read = transcript === null || transcript === void 0 || transcript === "" ? void 0 : yield* transcripts.read(owner, harness, [transcript]);
34327
- const tokens = read === void 0 ? turn.tokens : addRead(turn.tokens, read, true);
35146
+ const reads = read === void 0 ? turn.reads : addRead(turn.reads, read, { own: true });
34328
35147
  yield* client.sessions.turnFacts({
34329
35148
  params: {
34330
35149
  session,
@@ -34334,7 +35153,7 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
34334
35153
  generation: owner.generation,
34335
35154
  facts: {
34336
35155
  ...turn.facts,
34337
- tokens: turnTokens(tokens)
35156
+ ...readFacts(reads, turn.startedAt)
34338
35157
  }
34339
35158
  }
34340
35159
  });
@@ -34881,25 +35700,35 @@ const wordsOf = (report, home) => {
34881
35700
  }, ...check.next === void 0 ? [] : [{ command: check.next }]])];
34882
35701
  };
34883
35702
  const refused = () => Effect.succeed({ kind: "refused" });
34884
- const whoIs = Effect.fnUntraced(function* (token) {
34885
- const config = yield* Ref0Config;
34886
- return yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)))).me.get().pipe(Effect.timeout("10 seconds"), Effect.map((me) => ({
34887
- kind: "accepted",
34888
- me
34889
- })), Effect.catchTags({
34890
- Unauthenticated: refused,
34891
- SessionHasEnded: refused
34892
- }), Effect.orElseSucceed(() => ({ kind: "unanswered" })));
34893
- });
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))));
34894
35719
  const unansweredWords = (api) => `ref0 didn’t answer at ${api}.`;
34895
35720
  /** Check the grant kept for this folder, without borrowing another folder's authority. */
34896
35721
  const grantChecks = Effect.fnUntraced(function* (grant, configFolder) {
34897
- 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
+ }));
34898
35726
  const connected = checkOf("connected", "yes", "this machine holds its grant");
34899
35727
  switch (asked.kind) {
34900
35728
  case "accepted": return [connected, checkOf("accepted", "yes", "ref0 accepts its grant")];
34901
35729
  case "refused": return [connected, checkOf("accepted", "no", "ref0 refuses its grant", yield* connectLineFor(grant.harness, configFolder))];
34902
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)];
34903
35732
  }
34904
35733
  });
34905
35734
  /** Protocol mismatches either update this CLI or reinstall this folder's plugin. */
@@ -34991,11 +35820,12 @@ const sessionCheck = Effect.fnUntraced(function* (harness, configFolder) {
34991
35820
  const token = yield* Effect.result((yield* Keychain).get(sessionAccount(binding.value)));
34992
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);
34993
35822
  if (Option.isNone(token.success)) return checkOf("session", "no", "start a new session in this folder", launch);
34994
- const asked = yield* whoIs(token.success.value);
35823
+ const asked = yield* Effect.flatMap(sessionClient(token.success.value), whoIs);
34995
35824
  switch (asked.kind) {
34996
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}`);
34997
35826
  case "refused": return checkOf("session", "no", "ref0 no longer follows this session", launch);
34998
- case "unanswered": return checkOf("session", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status");
35827
+ case "unanswered":
35828
+ case "unsigned": return checkOf("session", "unknown", unansweredWords((yield* Ref0Config).api), "ref0 status");
34999
35829
  }
35000
35830
  });
35001
35831
  const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ json: asJson }) => Effect.gen(function* () {
@@ -35099,9 +35929,19 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
35099
35929
  Ref0Config.layer,
35100
35930
  sessionToken,
35101
35931
  FetchHttpClient.layer
35102
- ])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
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"])));
35103
35943
  //#endregion
35104
35944
  //#region src/main.ts
35105
- ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);
35945
+ ref0.pipe(Command.run({ version: cliVersion }), atRoot(services), NodeRuntime.runMain({ disableErrorReporting: true }));
35106
35946
  //#endregion
35107
35947
  export {};