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