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