@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.
- package/dist/main.mjs +1424 -584
- 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 {
|
|
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({
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
11361
|
+
model: Schema.optionalKey(Name),
|
|
11042
11362
|
modelSource: ModelSource,
|
|
11043
11363
|
/** Tool calls by tool name. */
|
|
11044
|
-
tools: Schema.Record(
|
|
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
|
-
|
|
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
|
|
11066
|
-
* and to the session's accountable person. A thread's totals still go to
|
|
11067
|
-
* `ThreadUsage`, so on a thread with one person's sessions they are that
|
|
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,
|
|
15218
|
-
*
|
|
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
|
|
15374
|
-
* confirming it saved the grant, or aborting, with its collecting
|
|
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,
|
|
21347
|
-
* the token, so a folder with a grant always knows which agent its
|
|
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.
|
|
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,
|
|
21709
|
-
*
|
|
21710
|
-
*
|
|
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*
|
|
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$
|
|
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$
|
|
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*
|
|
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
|
-
/**
|
|
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)
|
|
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({
|
|
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
|
|
32660
|
-
* since its tokens would go missing unseen. A line that isn't JSON is passed
|
|
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
|
|
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)
|
|
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(
|
|
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
|
-
|
|
32729
|
-
|
|
32730
|
-
|
|
32731
|
-
|
|
32732
|
-
|
|
32733
|
-
|
|
32734
|
-
|
|
32735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32754
|
-
|
|
32755
|
-
|
|
32756
|
-
|
|
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,
|
|
32771
|
-
|
|
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
|
-
|
|
32775
|
-
|
|
32776
|
-
|
|
32777
|
-
|
|
32778
|
-
}
|
|
32779
|
-
|
|
32780
|
-
|
|
32781
|
-
|
|
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
|
-
|
|
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 =
|
|
32920
|
-
|
|
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
|
-
|
|
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*
|
|
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
|
-
|
|
34576
|
+
reads: now.seen,
|
|
34577
|
+
startedAt: now.seen.startedAt
|
|
33788
34578
|
}) : Option.none();
|
|
33789
34579
|
/**
|
|
33790
|
-
* The transcripts a hook's read takes, and
|
|
33791
|
-
*
|
|
33792
|
-
*
|
|
33793
|
-
*
|
|
33794
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
34885
|
-
|
|
34886
|
-
|
|
34887
|
-
|
|
34888
|
-
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
|
|
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*
|
|
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*
|
|
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":
|
|
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 }),
|
|
35945
|
+
ref0.pipe(Command.run({ version: cliVersion }), atRoot(services), NodeRuntime.runMain({ disableErrorReporting: true }));
|
|
35106
35946
|
//#endregion
|
|
35107
35947
|
export {};
|