@ref0/cli 0.13.0 → 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 +519 -429
- package/package.json +2 -2
package/dist/main.mjs
CHANGED
|
@@ -1038,7 +1038,7 @@ const TokenDelegated = eventKind("token.delegated", Schema.Struct({
|
|
|
1038
1038
|
/** The credential this one was made under, when there is one. */
|
|
1039
1039
|
parent: Schema.optionalKey(CredentialRef),
|
|
1040
1040
|
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
1041
|
-
/** The key it's bound to, by its thumbprint, for a host grant made with one. */
|
|
1041
|
+
/** The key it's bound to, by its thumbprint, for a host grant or a session's token made with one. */
|
|
1042
1042
|
cnf: Schema.optionalKey(KeyBinding)
|
|
1043
1043
|
}));
|
|
1044
1044
|
const TokenRevoked = eventKind("token.revoked", Schema.Struct({
|
|
@@ -5543,6 +5543,11 @@ Schema.Struct({
|
|
|
5543
5543
|
/** For a child session: when its parent's token expires, which the child's never outlives. */
|
|
5544
5544
|
notAfter: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
5545
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
|
+
/**
|
|
5546
5551
|
* The generations the session token pins, from the host grant that started it, or its parent;
|
|
5547
5552
|
* and in an organization's project, for a person's own agent, its attachment's authorization's
|
|
5548
5553
|
* and the agent's target generation there.
|
|
@@ -5743,6 +5748,11 @@ Schema.Struct({
|
|
|
5743
5748
|
session: SessionId,
|
|
5744
5749
|
pins: SessionPins,
|
|
5745
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),
|
|
5746
5756
|
cause: Schema.Struct({
|
|
5747
5757
|
log: ProjectId,
|
|
5748
5758
|
event: EventId
|
|
@@ -14750,7 +14760,10 @@ const HistoryPack = Schema.Struct({
|
|
|
14750
14760
|
first: MergeSequence,
|
|
14751
14761
|
last: MergeSequence
|
|
14752
14762
|
})),
|
|
14753
|
-
/**
|
|
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
|
+
*/
|
|
14754
14767
|
baseline: Schema.optionalKey(GitRepositoryId),
|
|
14755
14768
|
/** A deepen pack: how deep its baseline reaches with it and every pack before it. */
|
|
14756
14769
|
depth: Schema.optionalKey(Depth),
|
|
@@ -15379,6 +15392,7 @@ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
|
|
|
15379
15392
|
generation: AttachedSoFar
|
|
15380
15393
|
})]);
|
|
15381
15394
|
Context.Service()("ref0/contracts/Caller");
|
|
15395
|
+
Context.Service()("ref0/contracts/ProvedKey");
|
|
15382
15396
|
/** A browser session's cookie, as `/v1` reads it. */
|
|
15383
15397
|
const browserSession = HttpApiSecurity.apiKey({
|
|
15384
15398
|
in: "cookie",
|
|
@@ -15387,8 +15401,10 @@ const browserSession = HttpApiSecurity.apiKey({
|
|
|
15387
15401
|
/**
|
|
15388
15402
|
* Every endpoint's credential, checked before its handler runs: a bearer token, a token bound to
|
|
15389
15403
|
* a key with a proof that key signed (`Authorization: DPoP` and one `DPoP` header), or a browser
|
|
15390
|
-
* session's cookie. A request with a cookie and a token is refused before it gets here.
|
|
15391
|
-
*
|
|
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.
|
|
15392
15408
|
*/
|
|
15393
15409
|
var Authorization = class extends HttpApiMiddleware.Service()("ref0/contracts/Authorization", {
|
|
15394
15410
|
requiredForClient: true,
|
|
@@ -15430,7 +15446,6 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
|
|
|
15430
15446
|
* what to change. Any other part that fails its contract is answered with Effect's empty 400.
|
|
15431
15447
|
*/
|
|
15432
15448
|
var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
|
|
15433
|
-
Context.Service()("ref0/contracts/ProvedKey");
|
|
15434
15449
|
/**
|
|
15435
15450
|
* A request with no credential, signed by a key it names, such as a machine's ask to connect: one
|
|
15436
15451
|
* `DPoP` header, a proof with no `ath`, since there's no token, checked before the request is
|
|
@@ -19303,6 +19318,21 @@ const dpopCredential = (credential) => Layer.mergeAll(HttpApiMiddleware.layerCli
|
|
|
19303
19318
|
*/
|
|
19304
19319
|
const boundClient = (credential) => ref0Client(credential.origin).pipe(Effect.provide(dpopCredential(credential)));
|
|
19305
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
|
+
/**
|
|
19306
19336
|
* A request with no credential, signed by the key it names, such as a machine's ask to connect:
|
|
19307
19337
|
* a proof its key store signs for each attempt, with no `ath`, since there's no token. Any other
|
|
19308
19338
|
* request is sent with nothing added.
|
|
@@ -19316,7 +19346,7 @@ const keyProof = (signer) => Layer.mergeAll(HttpApiMiddleware.layerClient(KeyPro
|
|
|
19316
19346
|
const provingClient = (signer) => ref0Client(signer.origin).pipe(Effect.provide(keyProof(signer)));
|
|
19317
19347
|
//#endregion
|
|
19318
19348
|
//#region ../sdk/src/client/request-id.ts
|
|
19319
|
-
const decodeRequestId
|
|
19349
|
+
const decodeRequestId = Schema.decodeUnknownEffect(RequestId);
|
|
19320
19350
|
/**
|
|
19321
19351
|
* A new delivery ID, for a request that changes something's `Idempotency-Key`: a UUIDv7 from the
|
|
19322
19352
|
* clock and ten random bytes, so a retry sends the same one and gets the first answer.
|
|
@@ -19325,7 +19355,7 @@ const newRequestId = Effect.gen(function* () {
|
|
|
19325
19355
|
const millis = yield* Clock.currentTimeMillis;
|
|
19326
19356
|
const random = /* @__PURE__ */ new Uint8Array(10);
|
|
19327
19357
|
for (let index = 0; index < random.length; index += 1) random[index] = yield* Random.nextIntBetween(0, 255);
|
|
19328
|
-
return yield* Effect.orDie(decodeRequestId
|
|
19358
|
+
return yield* Effect.orDie(decodeRequestId(`request_${encodeSuffix(uuidV7(millis, random))}`));
|
|
19329
19359
|
});
|
|
19330
19360
|
//#endregion
|
|
19331
19361
|
//#region src/sha256.ts
|
|
@@ -19396,6 +19426,317 @@ var Ref0Config = class Ref0Config extends Context.Service()("ref0/cli/Ref0Config
|
|
|
19396
19426
|
})).pipe(Layer.provide(NodeServices.layer));
|
|
19397
19427
|
};
|
|
19398
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
|
|
19399
19740
|
//#region src/secret.ts
|
|
19400
19741
|
const hashed = (secret) => Effect.gen(function* () {
|
|
19401
19742
|
const digest = yield* Effect.promise(async () => await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
|
|
@@ -19598,15 +19939,36 @@ const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] :
|
|
|
19598
19939
|
//#endregion
|
|
19599
19940
|
//#region src/session.ts
|
|
19600
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
|
+
/**
|
|
19601
19963
|
* An agent's command in a harness session that has no ref0 session, though an agent is connected
|
|
19602
19964
|
* for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
|
|
19603
19965
|
* person instead.
|
|
19604
19966
|
*/
|
|
19605
19967
|
var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
|
|
19606
19968
|
/**
|
|
19607
|
-
* The
|
|
19608
|
-
* harness bound when the session started. Where it's found is the harness's to say.
|
|
19609
|
-
* 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.
|
|
19610
19972
|
*/
|
|
19611
19973
|
var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
|
|
19612
19974
|
/** No session: until a harness binds one, the person asks, by approval. */
|
|
@@ -19623,9 +19985,11 @@ var SessionThread = class SessionThread extends Context.Service()("ref0/cli/Sess
|
|
|
19623
19985
|
//#endregion
|
|
19624
19986
|
//#region src/call.ts
|
|
19625
19987
|
/**
|
|
19626
|
-
* Every command's one way to ref0: an endpoint's request, sent
|
|
19627
|
-
*
|
|
19628
|
-
* 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.
|
|
19629
19993
|
*/
|
|
19630
19994
|
/** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
|
|
19631
19995
|
var CallFailed = class extends Schema.TaggedError()("CallFailed", {
|
|
@@ -19682,7 +20046,7 @@ const refusedBy = (status, refusal, change) => new CallFailed({
|
|
|
19682
20046
|
* An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
|
|
19683
20047
|
* the CLI made. A change it answered may have run.
|
|
19684
20048
|
*/
|
|
19685
|
-
const unreadable
|
|
20049
|
+
const unreadable = (endpoint, change) => new CallFailed({
|
|
19686
20050
|
status: 0,
|
|
19687
20051
|
kind: "Unexpected",
|
|
19688
20052
|
nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
|
|
@@ -19731,7 +20095,7 @@ const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, e
|
|
|
19731
20095
|
const state = answered.value;
|
|
19732
20096
|
heardAt = now;
|
|
19733
20097
|
if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
|
|
19734
|
-
onNone: () => Effect.fail(unreadable
|
|
20098
|
+
onNone: () => Effect.fail(unreadable(endpoint, change)),
|
|
19735
20099
|
onSome: Result.match({
|
|
19736
20100
|
onSuccess: () => Effect.succeed(state.result),
|
|
19737
20101
|
onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
|
|
@@ -19761,42 +20125,52 @@ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
|
|
|
19761
20125
|
const config = yield* Ref0Config;
|
|
19762
20126
|
const session = yield* SessionToken;
|
|
19763
20127
|
const http = yield* HttpClient.HttpClient;
|
|
19764
|
-
const
|
|
20128
|
+
const signing = yield* Effect.context();
|
|
20129
|
+
const asSession = Effect.fnUntraced(function* (kept, request, endpoint) {
|
|
19765
20130
|
const timeout = timeoutOf(request.command.endpoint);
|
|
19766
|
-
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));
|
|
19767
20137
|
const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
|
|
19768
20138
|
const change = request.method !== "GET";
|
|
19769
20139
|
const lost = change ? unanswered$1 : unreachable;
|
|
19770
|
-
const { response, text } = yield*
|
|
20140
|
+
const { response, text } = yield* send(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
|
|
19771
20141
|
response,
|
|
19772
20142
|
text
|
|
20143
|
+
}))), Effect.catchTag("KeyStoreUnavailable", (unsigned) => Effect.fail(new CallFailed({
|
|
20144
|
+
status: 0,
|
|
20145
|
+
kind: unsigned._tag,
|
|
20146
|
+
nextStep: unsigned.nextStep
|
|
19773
20147
|
}))), Effect.timeoutOrElse({
|
|
19774
20148
|
duration: timeout,
|
|
19775
20149
|
orElse: () => Effect.fail(lost)
|
|
19776
|
-
}), Effect.mapError(() => lost));
|
|
20150
|
+
}), Effect.mapError((error) => error instanceof CallFailed ? error : lost));
|
|
19777
20151
|
const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
|
|
19778
20152
|
if (response.status === 202) {
|
|
19779
20153
|
const needed = decodeNeeded(body);
|
|
19780
20154
|
if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
|
|
19781
20155
|
yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
|
|
19782
|
-
return yield* waitForAnswer(yield* collectAsSession(
|
|
20156
|
+
return yield* waitForAnswer(yield* collectAsSession(kept, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
|
|
19783
20157
|
}
|
|
19784
20158
|
return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
|
|
19785
20159
|
});
|
|
19786
|
-
const collectAsSession = Effect.fnUntraced(function* (
|
|
19787
|
-
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) {
|
|
19788
20161
|
const refused = (status) => (error) => Effect.fail(new CallFailed({
|
|
19789
20162
|
status,
|
|
19790
20163
|
kind: error._tag,
|
|
19791
20164
|
nextStep: error.nextStep
|
|
19792
20165
|
}));
|
|
19793
|
-
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({
|
|
19794
20167
|
duration: requestTimeout$1,
|
|
19795
20168
|
orElse: () => Effect.succeedNone
|
|
19796
20169
|
}), Effect.catchTags({
|
|
19797
20170
|
ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
|
|
19798
20171
|
Unauthenticated: refused(401),
|
|
19799
|
-
SessionHasEnded: refused(410)
|
|
20172
|
+
SessionHasEnded: refused(410),
|
|
20173
|
+
InvalidDpopProof: refused(401)
|
|
19800
20174
|
}), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19801
20175
|
});
|
|
19802
20176
|
const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
|
|
@@ -19833,22 +20207,22 @@ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
|
|
|
19833
20207
|
return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
|
|
19834
20208
|
});
|
|
19835
20209
|
const call = Effect.fn("Ref0.call")(function* (command) {
|
|
19836
|
-
const
|
|
20210
|
+
const kept = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
|
|
19837
20211
|
status: 0,
|
|
19838
20212
|
kind: error._tag,
|
|
19839
20213
|
nextStep: error.nextStep
|
|
19840
20214
|
})));
|
|
19841
|
-
const request = yield* (Option.isSome(
|
|
20215
|
+
const request = yield* (Option.isSome(kept) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
|
|
19842
20216
|
status: 400,
|
|
19843
20217
|
kind: refused._tag,
|
|
19844
20218
|
nextStep: refused.nextStep
|
|
19845
20219
|
})));
|
|
19846
20220
|
const change = request.method !== "GET";
|
|
19847
|
-
const answer = yield* Option.match(
|
|
20221
|
+
const answer = yield* Option.match(kept, {
|
|
19848
20222
|
onNone: () => asPerson(request.command, change, command.endpoint),
|
|
19849
|
-
onSome: (
|
|
20223
|
+
onSome: (one) => asSession(one, request, command.endpoint)
|
|
19850
20224
|
});
|
|
19851
|
-
if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable
|
|
20225
|
+
if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
|
|
19852
20226
|
return answer;
|
|
19853
20227
|
});
|
|
19854
20228
|
return Ref0.of({ call });
|
|
@@ -19863,7 +20237,7 @@ const callEndpoint = (command) => Effect.gen(function* () {
|
|
|
19863
20237
|
return {
|
|
19864
20238
|
json,
|
|
19865
20239
|
value: yield* Option.match(successOf(command.endpoint, json), {
|
|
19866
|
-
onNone: () => Effect.fail(unreadable
|
|
20240
|
+
onNone: () => Effect.fail(unreadable(command.endpoint, false)),
|
|
19867
20241
|
onSome: Effect.succeed
|
|
19868
20242
|
})
|
|
19869
20243
|
};
|
|
@@ -21528,287 +21902,6 @@ const connectLineFor = Effect.fnUntraced(function* (harness, configFolder) {
|
|
|
21528
21902
|
return `${configFolders[harness].variable}=${shellWord(configFolder)} ref0 connect`;
|
|
21529
21903
|
});
|
|
21530
21904
|
//#endregion
|
|
21531
|
-
//#region src/keychain.ts
|
|
21532
|
-
/**
|
|
21533
|
-
* The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
|
|
21534
|
-
* makes, and a session's token. Never a file. On macOS through `security`, on Linux through
|
|
21535
|
-
* `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
|
|
21536
|
-
* an argument, since any process on the machine can read another's arguments.
|
|
21537
|
-
*/
|
|
21538
|
-
var KeychainError = class extends Schema.TaggedError()("KeychainError", {
|
|
21539
|
-
nextStep: Schema.String,
|
|
21540
|
-
/** The keychain answered, with an item ref0 can't read, rather than not answering. */
|
|
21541
|
-
unreadable: Schema.optionalKey(Schema.Literal(true))
|
|
21542
|
-
}) {};
|
|
21543
|
-
/**
|
|
21544
|
-
* The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
|
|
21545
|
-
* grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
|
|
21546
|
-
*/
|
|
21547
|
-
const keychainIn = Effect.fnUntraced(function* (service) {
|
|
21548
|
-
const crypto = yield* Crypto.Crypto;
|
|
21549
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
21550
|
-
const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
|
|
21551
|
-
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." });
|
|
21552
|
-
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." });
|
|
21553
|
-
const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
|
|
21554
|
-
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));
|
|
21555
|
-
const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
|
|
21556
|
-
const code = yield* handle.exitCode;
|
|
21557
|
-
return {
|
|
21558
|
-
output: output.trim(),
|
|
21559
|
-
complaint: complaint.trim(),
|
|
21560
|
-
code,
|
|
21561
|
-
succeeded: code === ChildProcessSpawner.ExitCode(0)
|
|
21562
|
-
};
|
|
21563
|
-
})).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
|
|
21564
|
-
const get = Effect.fn("Keychain.get")(function* (account) {
|
|
21565
|
-
const found = macOS ? yield* run("security", [
|
|
21566
|
-
"find-generic-password",
|
|
21567
|
-
"-s",
|
|
21568
|
-
service,
|
|
21569
|
-
"-a",
|
|
21570
|
-
account,
|
|
21571
|
-
"-w"
|
|
21572
|
-
], "") : yield* run("secret-tool", [
|
|
21573
|
-
"lookup",
|
|
21574
|
-
"service",
|
|
21575
|
-
service,
|
|
21576
|
-
"account",
|
|
21577
|
-
account
|
|
21578
|
-
], "");
|
|
21579
|
-
if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
|
|
21580
|
-
if (macOS) {
|
|
21581
|
-
if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
|
|
21582
|
-
return yield* unavailable;
|
|
21583
|
-
}
|
|
21584
|
-
if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
|
|
21585
|
-
const listed = yield* run("secret-tool", [
|
|
21586
|
-
"search",
|
|
21587
|
-
"--all",
|
|
21588
|
-
"service",
|
|
21589
|
-
service,
|
|
21590
|
-
"account",
|
|
21591
|
-
account
|
|
21592
|
-
], "");
|
|
21593
|
-
if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
|
|
21594
|
-
return Option.none();
|
|
21595
|
-
});
|
|
21596
|
-
const set = Effect.fn("Keychain.set")(function* (account, secret) {
|
|
21597
|
-
const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
|
|
21598
|
-
const value = Redacted.value(secret);
|
|
21599
|
-
if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
|
|
21600
|
-
const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
|
|
21601
|
-
if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
|
|
21602
|
-
if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
|
|
21603
|
-
"store",
|
|
21604
|
-
"--label",
|
|
21605
|
-
`ref0 ${account}`,
|
|
21606
|
-
"service",
|
|
21607
|
-
service,
|
|
21608
|
-
"account",
|
|
21609
|
-
account
|
|
21610
|
-
], Redacted.value(secret))).succeeded) return yield* unavailable;
|
|
21611
|
-
});
|
|
21612
|
-
const remove = Effect.fn("Keychain.remove")(function* (account) {
|
|
21613
|
-
if (!(yield* macOS ? run("security", [
|
|
21614
|
-
"delete-generic-password",
|
|
21615
|
-
"-s",
|
|
21616
|
-
service,
|
|
21617
|
-
"-a",
|
|
21618
|
-
account
|
|
21619
|
-
], "") : run("secret-tool", [
|
|
21620
|
-
"clear",
|
|
21621
|
-
"service",
|
|
21622
|
-
service,
|
|
21623
|
-
"account",
|
|
21624
|
-
account
|
|
21625
|
-
], "")).succeeded) return yield* unavailable;
|
|
21626
|
-
});
|
|
21627
|
-
const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
|
|
21628
|
-
return {
|
|
21629
|
-
probe: Effect.gen(function* () {
|
|
21630
|
-
const account = `connect-probe/${yield* random}`;
|
|
21631
|
-
const value = Redacted.make(yield* random);
|
|
21632
|
-
yield* Effect.gen(function* () {
|
|
21633
|
-
yield* set(account, value);
|
|
21634
|
-
const kept = yield* get(account);
|
|
21635
|
-
if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
|
|
21636
|
-
yield* remove(account);
|
|
21637
|
-
}).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
|
|
21638
|
-
}).pipe(Effect.timeoutOrElse({
|
|
21639
|
-
duration: Duration.minutes(1),
|
|
21640
|
-
orElse: () => Effect.fail(unavailable)
|
|
21641
|
-
})),
|
|
21642
|
-
get,
|
|
21643
|
-
set,
|
|
21644
|
-
remove
|
|
21645
|
-
};
|
|
21646
|
-
});
|
|
21647
|
-
var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
|
|
21648
|
-
static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
|
|
21649
|
-
};
|
|
21650
|
-
//#endregion
|
|
21651
|
-
//#region src/machine-key.ts
|
|
21652
|
-
/**
|
|
21653
|
-
* This machine's key, which signs a proof for its ask to connect and for each request a host
|
|
21654
|
-
* grant makes: one ES256 key for each OS account and origin, in the origin's keychain service
|
|
21655
|
-
* under account `machine-key`, as `{ store: 'software', jwk }`. `ref0 connect` makes it, under its
|
|
21656
|
-
* lock, only when the keychain says it holds none, and nothing else makes one. A process reads it
|
|
21657
|
-
* once and keeps it, since each read is a `security` or `secret-tool` process. Its private half
|
|
21658
|
-
* goes to the keychain on stdin and into a Web Crypto key that can't be exported, and nowhere
|
|
21659
|
-
* else: no argument, environment, config, error or log.
|
|
21660
|
-
*/
|
|
21661
|
-
/** The keychain account the key is kept under. */
|
|
21662
|
-
const account = "machine-key";
|
|
21663
|
-
/** How long reading and importing the key may take for a request, and signing with it. */
|
|
21664
|
-
const deadline = Duration.seconds(3);
|
|
21665
|
-
/**
|
|
21666
|
-
* How long `ref0 connect` may wait on the keychain to read, make and keep the key, as its probe
|
|
21667
|
-
* may: a prompt to unlock the keychain can take that long.
|
|
21668
|
-
*/
|
|
21669
|
-
const connectDeadline = Duration.minutes(1);
|
|
21670
|
-
/** A P-256 coordinate or private scalar: 32 bytes, in base64url. */
|
|
21671
|
-
const Scalar = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/));
|
|
21672
|
-
/** A P-256 private key as a JWK: its public point, and its private scalar `d`. */
|
|
21673
|
-
const PrivateJwk = Schema.Struct({
|
|
21674
|
-
kty: Schema.Literal("EC"),
|
|
21675
|
-
crv: Schema.Literal("P-256"),
|
|
21676
|
-
x: Scalar,
|
|
21677
|
-
y: Scalar,
|
|
21678
|
-
d: Scalar
|
|
21679
|
-
});
|
|
21680
|
-
/** The keychain's record: which store keeps the key, and the key. A hardware store keeps its own. */
|
|
21681
|
-
const KeptKey = Schema.Struct({
|
|
21682
|
-
store: Schema.Literal("software"),
|
|
21683
|
-
jwk: PrivateJwk
|
|
21684
|
-
});
|
|
21685
|
-
const decodeKept$3 = Schema.decodeUnknownOption(Schema.fromJsonString(KeptKey));
|
|
21686
|
-
const encodeKept$3 = Schema.encodeSync(Schema.fromJsonString(KeptKey));
|
|
21687
|
-
const decodePrivateJwk = Schema.decodeUnknownEffect(PrivateJwk);
|
|
21688
|
-
const decodePublicJwk = Schema.decodeSync(PublicJwk);
|
|
21689
|
-
const ecdsa = {
|
|
21690
|
-
name: "ECDSA",
|
|
21691
|
-
namedCurve: "P-256"
|
|
21692
|
-
};
|
|
21693
|
-
const es256 = {
|
|
21694
|
-
name: "ECDSA",
|
|
21695
|
-
hash: "SHA-256"
|
|
21696
|
-
};
|
|
21697
|
-
/** What a key signs to prove it signs for its public half. */
|
|
21698
|
-
const proved = new TextEncoder().encode("ref0 machine key");
|
|
21699
|
-
const timedOut = new KeychainError({ nextStep: "The keychain didn’t answer in time. Check that it’s unlocked, and try again." });
|
|
21700
|
-
const unreadable = new KeychainError({ nextStep: "ref0 can’t read this machine’s key in the keychain, so it signs nothing and makes no other. Update ref0, which may have kept it in a newer form, or repair the keychain, and try again." });
|
|
21701
|
-
const notKept = new KeychainError({ nextStep: "The keychain didn’t keep this machine’s key. Check that it’s unlocked, then run ref0 connect again." });
|
|
21702
|
-
const windows = new KeychainError({ nextStep: "ref0 connect doesn’t run on Windows yet: it keeps this machine’s key in the macOS keychain or a Linux secret service. Run it in WSL instead." });
|
|
21703
|
-
const didNotSign = new KeyStoreUnavailable({ nextStep: "This machine’s key didn’t sign in time. Try again." });
|
|
21704
|
-
/** A fresh key's private JWK, with only the members the record keeps. */
|
|
21705
|
-
const freshKey = Effect.promise(async () => {
|
|
21706
|
-
const pair = await crypto.subtle.generateKey(ecdsa, true, ["sign", "verify"]);
|
|
21707
|
-
return await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
21708
|
-
}).pipe(Effect.flatMap(({ kty, crv, x, y, d }) => decodePrivateJwk({
|
|
21709
|
-
kty,
|
|
21710
|
-
crv,
|
|
21711
|
-
x,
|
|
21712
|
-
y,
|
|
21713
|
-
d
|
|
21714
|
-
})), Effect.orDie);
|
|
21715
|
-
/**
|
|
21716
|
-
* A key store over a private JWK: imported once, as a key that can't be exported, and proved to
|
|
21717
|
-
* sign for its public half, or it's no key to bind anything to.
|
|
21718
|
-
*/
|
|
21719
|
-
const softwareStore = Effect.fnUntraced(function* (jwk) {
|
|
21720
|
-
const publicKey = decodePublicJwk({
|
|
21721
|
-
kty: jwk.kty,
|
|
21722
|
-
crv: jwk.crv,
|
|
21723
|
-
x: jwk.x,
|
|
21724
|
-
y: jwk.y
|
|
21725
|
-
});
|
|
21726
|
-
const signs = yield* Effect.tryPromise({
|
|
21727
|
-
try: async () => {
|
|
21728
|
-
const [signing, verifying] = await Promise.all([crypto.subtle.importKey("jwk", jwk, ecdsa, false, ["sign"]), crypto.subtle.importKey("jwk", publicKey, ecdsa, false, ["verify"])]);
|
|
21729
|
-
const signature = await crypto.subtle.sign(es256, signing, proved);
|
|
21730
|
-
return await crypto.subtle.verify(es256, verifying, signature, proved) ? signing : void 0;
|
|
21731
|
-
},
|
|
21732
|
-
catch: () => unreadable
|
|
21733
|
-
});
|
|
21734
|
-
if (signs === void 0) return yield* unreadable;
|
|
21735
|
-
const sign = (bytes) => Effect.tryPromise({
|
|
21736
|
-
try: async () => new Uint8Array(await crypto.subtle.sign(es256, signs, new Uint8Array(bytes))),
|
|
21737
|
-
catch: () => didNotSign
|
|
21738
|
-
}).pipe(Effect.timeoutOrElse({
|
|
21739
|
-
duration: deadline,
|
|
21740
|
-
orElse: () => Effect.fail(didNotSign)
|
|
21741
|
-
}));
|
|
21742
|
-
return {
|
|
21743
|
-
store: {
|
|
21744
|
-
publicKey,
|
|
21745
|
-
sign
|
|
21746
|
-
},
|
|
21747
|
-
binding: { jkt: yield* keyThumbprint(publicKey) }
|
|
21748
|
-
};
|
|
21749
|
-
});
|
|
21750
|
-
/**
|
|
21751
|
-
* This machine's key, read once for the process. `supported` refuses a platform with no keychain
|
|
21752
|
-
* that keeps it, such as Windows, before `ref0 connect` asks the keychain anything. `ensure` is
|
|
21753
|
-
* `ref0 connect`'s: the key the keychain holds, or one made and kept now if it says it holds none,
|
|
21754
|
-
* under the connect lock, once the keychain is probed. `boundTo` is for a kept
|
|
21755
|
-
* credential bound to a key: the key it names, read again once if the one held is another, such
|
|
21756
|
-
* as one replaced since, or `InvalidDpopProof`, without sending anything, when the keychain has
|
|
21757
|
-
* none or another. Neither makes a key from an item it can't read.
|
|
21758
|
-
*/
|
|
21759
|
-
var MachineKey = class MachineKey extends Context.Service()("ref0/cli/MachineKey") {
|
|
21760
|
-
/** On the platform Node names, such as `win32`, where no keychain keeps the key yet. */
|
|
21761
|
-
static layerOn = (platform) => Layer.effect(MachineKey, Effect.gen(function* () {
|
|
21762
|
-
const keychain = yield* Keychain;
|
|
21763
|
-
const held = yield* Ref.make(Option.none());
|
|
21764
|
-
const reading = yield* Semaphore.make(1);
|
|
21765
|
-
const read = Effect.gen(function* () {
|
|
21766
|
-
const kept = yield* keychain.get(account);
|
|
21767
|
-
if (Option.isNone(kept)) return Option.none();
|
|
21768
|
-
const record = decodeKept$3(Redacted.value(kept.value));
|
|
21769
|
-
if (Option.isNone(record)) return yield* unreadable;
|
|
21770
|
-
const key = yield* softwareStore(record.value.jwk);
|
|
21771
|
-
yield* Ref.set(held, Option.some(key));
|
|
21772
|
-
return Option.some(key);
|
|
21773
|
-
});
|
|
21774
|
-
const made = Effect.gen(function* () {
|
|
21775
|
-
const jwk = yield* freshKey;
|
|
21776
|
-
yield* keychain.set(account, Redacted.make(encodeKept$3({
|
|
21777
|
-
store: "software",
|
|
21778
|
-
jwk
|
|
21779
|
-
})));
|
|
21780
|
-
const back = yield* read;
|
|
21781
|
-
const same = Option.exists(back, (key) => key.store.publicKey.x === jwk.x && key.store.publicKey.y === jwk.y);
|
|
21782
|
-
if (Option.isNone(back) || !same) return yield* notKept;
|
|
21783
|
-
return back.value;
|
|
21784
|
-
});
|
|
21785
|
-
const supported = platform === "win32" ? Effect.fail(windows) : Effect.void;
|
|
21786
|
-
const ensure = Effect.gen(function* () {
|
|
21787
|
-
const kept = yield* read;
|
|
21788
|
-
return Option.isSome(kept) ? kept.value : yield* made;
|
|
21789
|
-
}).pipe(Effect.timeoutOrElse({
|
|
21790
|
-
duration: connectDeadline,
|
|
21791
|
-
orElse: () => Effect.fail(timedOut)
|
|
21792
|
-
}), reading.withPermits(1));
|
|
21793
|
-
const boundTo = (binding) => Effect.gen(function* () {
|
|
21794
|
-
const now = yield* Ref.get(held);
|
|
21795
|
-
if (Option.isSome(now) && now.value.binding.jkt === binding.jkt) return now.value.store;
|
|
21796
|
-
const kept = yield* read.pipe(Effect.timeoutOrElse({
|
|
21797
|
-
duration: deadline,
|
|
21798
|
-
orElse: () => Effect.fail(timedOut)
|
|
21799
|
-
}));
|
|
21800
|
-
if (Option.isSome(kept) && kept.value.binding.jkt === binding.jkt) return kept.value.store;
|
|
21801
|
-
return yield* invalidDpopProof;
|
|
21802
|
-
}).pipe(reading.withPermits(1));
|
|
21803
|
-
return MachineKey.of({
|
|
21804
|
-
supported,
|
|
21805
|
-
ensure,
|
|
21806
|
-
boundTo
|
|
21807
|
-
});
|
|
21808
|
-
}));
|
|
21809
|
-
static layer = MachineKey.layerOn(process.platform);
|
|
21810
|
-
};
|
|
21811
|
-
//#endregion
|
|
21812
21905
|
//#region src/host-grant.ts
|
|
21813
21906
|
/**
|
|
21814
21907
|
* All lasting authority for one config folder, stored as one keychain value: the agent the folder
|
|
@@ -21832,6 +21925,15 @@ const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
|
|
|
21832
21925
|
* read as a grant, such as one an earlier ref0 kept, still names the grant it held.
|
|
21833
21926
|
*/
|
|
21834
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
|
+
}));
|
|
21835
21937
|
/** Grants in the origin’s keychain service, account host-grant/<resolved config folder>. */
|
|
21836
21938
|
var HostGrant = class HostGrant extends Context.Service()("ref0/cli/HostGrant") {
|
|
21837
21939
|
static layer = Layer.effect(HostGrant, Effect.gen(function* () {
|
|
@@ -21867,24 +21969,6 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
|
21867
21969
|
}
|
|
21868
21970
|
return Option.none();
|
|
21869
21971
|
});
|
|
21870
|
-
const decodeProofOrigin$1 = Schema.decodeUnknownEffect(ProofOrigin);
|
|
21871
|
-
/**
|
|
21872
|
-
* ref0's `/v1` as a typed client acting with a kept host grant: with a proof this machine's key
|
|
21873
|
-
* signs for each request when the grant is bound to it, or as a bearer token when it's a grant
|
|
21874
|
-
* from before binding. A grant bound to a key the keychain no longer holds is refused here, with
|
|
21875
|
-
* `InvalidDpopProof`, and nothing is sent.
|
|
21876
|
-
*/
|
|
21877
|
-
const grantClient = Effect.fnUntraced(function* (grant) {
|
|
21878
|
-
const config = yield* Ref0Config;
|
|
21879
|
-
if (grant.cnf === void 0) return yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.token)));
|
|
21880
|
-
const store = yield* (yield* MachineKey).boundTo(grant.cnf);
|
|
21881
|
-
const origin = yield* Effect.orDie(decodeProofOrigin$1(config.api));
|
|
21882
|
-
return yield* boundClient({
|
|
21883
|
-
origin,
|
|
21884
|
-
store,
|
|
21885
|
-
token: grant.token
|
|
21886
|
-
});
|
|
21887
|
-
});
|
|
21888
21972
|
//#endregion
|
|
21889
21973
|
//#region src/version.ts
|
|
21890
21974
|
/**
|
|
@@ -21893,7 +21977,7 @@ const grantClient = Effect.fnUntraced(function* (grant) {
|
|
|
21893
21977
|
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
21894
21978
|
* and only as a default export.
|
|
21895
21979
|
*/
|
|
21896
|
-
const cliVersion = "0.
|
|
21980
|
+
const cliVersion = "0.14.0";
|
|
21897
21981
|
//#endregion
|
|
21898
21982
|
//#region src/harness/hook-protocol.ts
|
|
21899
21983
|
/**
|
|
@@ -27203,10 +27287,9 @@ const gitTokenOf = Effect.fnUntraced(function* (binding) {
|
|
|
27203
27287
|
const keychain = yield* Keychain;
|
|
27204
27288
|
const kept = yield* keychain.get(gitTokenAccount(binding));
|
|
27205
27289
|
if (Option.isSome(kept)) return kept;
|
|
27206
|
-
const
|
|
27207
|
-
if (Option.isNone(
|
|
27208
|
-
const
|
|
27209
|
-
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);
|
|
27210
27293
|
const me = yield* client.me.get();
|
|
27211
27294
|
if (me.session === void 0) return Option.none();
|
|
27212
27295
|
const { secret, hash } = yield* freshTokenSecret;
|
|
@@ -27344,12 +27427,11 @@ const trimShallow = Effect.fnUntraced(function* (gitDir) {
|
|
|
27344
27427
|
* attempts the merge: the checkout's own for `ref0 sync`, or the local check's scratch one.
|
|
27345
27428
|
*/
|
|
27346
27429
|
const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
|
|
27347
|
-
const
|
|
27348
|
-
|
|
27349
|
-
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" });
|
|
27350
27432
|
const fs = yield* FileSystem.FileSystem;
|
|
27351
27433
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
27352
|
-
const client = yield*
|
|
27434
|
+
const client = yield* keptClient(kept.value);
|
|
27353
27435
|
const params = yield* decodePlace(repoParts(repo));
|
|
27354
27436
|
const manifest = yield* client.repos.manifest({ params });
|
|
27355
27437
|
const temp = yield* fs.makeTempDirectoryScoped();
|
|
@@ -30578,7 +30660,7 @@ const watchCommands = [Command.make("watch", {
|
|
|
30578
30660
|
owner: ownerName,
|
|
30579
30661
|
agent: flags.agent
|
|
30580
30662
|
});
|
|
30581
|
-
const client = yield*
|
|
30663
|
+
const client = yield* keptClient(grant.value);
|
|
30582
30664
|
const watching = {
|
|
30583
30665
|
project: flags.project,
|
|
30584
30666
|
parent: Option.getOrUndefined(flags.parent),
|
|
@@ -32452,7 +32534,7 @@ var BaseChecks = class BaseChecks extends Context.Service()("ref0/cli/BaseChecks
|
|
|
32452
32534
|
], { stdin: "ignore" })).pipe(Effect.map((code) => Number(code) === 0), Effect.orElseSucceed(() => false));
|
|
32453
32535
|
return BaseChecks.of({
|
|
32454
32536
|
settingsAt,
|
|
32455
|
-
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) }))),
|
|
32456
32538
|
holds
|
|
32457
32539
|
});
|
|
32458
32540
|
}));
|
|
@@ -32505,10 +32587,10 @@ const baseWatch = Effect.fnUntraced(function* (edges) {
|
|
|
32505
32587
|
return;
|
|
32506
32588
|
}
|
|
32507
32589
|
if (Option.isNone(repo)) return;
|
|
32508
|
-
const read = yield*
|
|
32590
|
+
const read = yield* clientOf(bound.kept).pipe(Effect.flatMap((client) => client.sessions.base({ params: {
|
|
32509
32591
|
session: bound.session,
|
|
32510
32592
|
repo: repo.value
|
|
32511
|
-
} })
|
|
32593
|
+
} })), Effect.option);
|
|
32512
32594
|
if (Option.isNone(read)) return;
|
|
32513
32595
|
const base = read.value;
|
|
32514
32596
|
const mark = {
|
|
@@ -32534,7 +32616,7 @@ const baseWatch = Effect.fnUntraced(function* (edges) {
|
|
|
32534
32616
|
commit: base.commit,
|
|
32535
32617
|
url,
|
|
32536
32618
|
ref: base.ref
|
|
32537
|
-
}, delivered, clone.within, bound.
|
|
32619
|
+
}, delivered, clone.within, bound.kept).pipe(Effect.option);
|
|
32538
32620
|
if (Option.isNone(checked)) return;
|
|
32539
32621
|
const notice = checked.value.kind === "compared" ? checked.value.notice : Option.none();
|
|
32540
32622
|
yield* Ref.update(watched, (was) => was.top !== clone.top ? was : {
|
|
@@ -34157,28 +34239,31 @@ function readHook(args) {
|
|
|
34157
34239
|
* `ref0 clone` writes), or, with none, on a new thread under a plain title, such as "Claude Code
|
|
34158
34240
|
* in shop, 9 Oct 14:02", which the agent then names in its own words (decided, 9 Oct 2026, 3).
|
|
34159
34241
|
* The prompt's words never become the title. A session `ref0 watch` started says so, with the
|
|
34160
|
-
* event that started its thread, as its trigger. The session's
|
|
34161
|
-
* under its binding's account, and nowhere else,
|
|
34162
|
-
*
|
|
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.
|
|
34163
34246
|
*/
|
|
34164
34247
|
/** The keychain account a binding's thread is kept under, beside its session token's. */
|
|
34165
34248
|
const threadAccount = (binding) => `${sessionAccount(binding)}/thread`;
|
|
34166
34249
|
/** The keychain account a start not yet landed is kept under, beside its session token's. */
|
|
34167
34250
|
const pendingAccount = (binding) => `${sessionAccount(binding)}/pending`;
|
|
34168
34251
|
const decodeRequest = Schema.decodeUnknownEffect(StartSessionRequest);
|
|
34169
|
-
const decodeRequestId = Schema.decodeUnknownOption(RequestId);
|
|
34170
34252
|
/**
|
|
34171
|
-
* A
|
|
34172
|
-
*
|
|
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.
|
|
34173
34256
|
*/
|
|
34174
|
-
const
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
|
|
34179
|
-
|
|
34180
|
-
|
|
34181
|
-
|
|
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;
|
|
34182
34267
|
/** A secret's SHA-256, as hex: all ref0 is told of it. */
|
|
34183
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(""));
|
|
34184
34269
|
/** How the harness is named in a plain title. */
|
|
@@ -34203,15 +34288,23 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
34203
34288
|
const folder = yield* (yield* ConfigFolders).folder(harness);
|
|
34204
34289
|
const grant = yield* hostGrant.get(folder);
|
|
34205
34290
|
if (Option.isNone(grant) || grant.value.harness !== harness) return Option.none();
|
|
34206
|
-
const
|
|
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);
|
|
34207
34295
|
const pending = pendingAccount(input.binding);
|
|
34208
|
-
const held = Option.flatMap(yield* keychain.get(sessionAccount(input.binding)), (kept) => parseToken(Redacted.value(kept)));
|
|
34209
|
-
const resent = Option.filter(Option.flatMap(yield* keychain.get(pending), (kept) => decodePending(Redacted.value(kept))), (one) => !Option.exists(held, (parts) => parts.secret === one.secret));
|
|
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));
|
|
34210
34298
|
const { requestId, secret } = Option.isSome(resent) ? resent.value : {
|
|
34211
34299
|
requestId: yield* newRequestId,
|
|
34212
34300
|
secret: encodeSecret(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))
|
|
34213
34301
|
};
|
|
34214
|
-
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
|
+
})));
|
|
34215
34308
|
const hash = yield* hashOf(secret);
|
|
34216
34309
|
const where = input.on.kind === "thread" ? threadParts(input.on.written) : {
|
|
34217
34310
|
owner: grant.value.owner,
|
|
@@ -34246,11 +34339,20 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
34246
34339
|
kind: "project",
|
|
34247
34340
|
name: newThreadProject
|
|
34248
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
|
+
};
|
|
34249
34348
|
yield* keychain.remove(gitTokenAccount(input.binding)).pipe(Effect.ignore);
|
|
34250
|
-
yield* keychain.set(sessionAccount(input.binding),
|
|
34349
|
+
yield* keychain.set(sessionAccount(input.binding), keptSessionValue(kept));
|
|
34251
34350
|
yield* keychain.set(threadAccount(input.binding), Redacted.make(thread)).pipe(Effect.ignore);
|
|
34252
34351
|
yield* keychain.remove(pending).pipe(Effect.ignore);
|
|
34253
|
-
return Option.some(
|
|
34352
|
+
return Option.some({
|
|
34353
|
+
started,
|
|
34354
|
+
kept
|
|
34355
|
+
});
|
|
34254
34356
|
});
|
|
34255
34357
|
//#endregion
|
|
34256
34358
|
//#region src/harness/watched-start.ts
|
|
@@ -34333,14 +34435,13 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
34333
34435
|
const owned = yield* Ref.make(Option.none());
|
|
34334
34436
|
const onTheWay = yield* Ref.make(Option.none());
|
|
34335
34437
|
const listen = Effect.fnUntraced(function* (bound) {
|
|
34336
|
-
const client = yield* clientOf(bound.token);
|
|
34337
34438
|
let pause = 1;
|
|
34338
34439
|
let opened = false;
|
|
34339
34440
|
for (;;) {
|
|
34340
|
-
const ticket = yield* client.live.ticket({ payload: { track: {
|
|
34441
|
+
const ticket = yield* clientOf(bound.kept).pipe(Effect.flatMap((client) => client.live.ticket({ payload: { track: {
|
|
34341
34442
|
kind: "session",
|
|
34342
34443
|
session: bound.session
|
|
34343
|
-
} } })
|
|
34444
|
+
} } })), Effect.option);
|
|
34344
34445
|
let closed;
|
|
34345
34446
|
if (Option.isSome(ticket)) {
|
|
34346
34447
|
if (opened) yield* edges.moved;
|
|
@@ -34366,12 +34467,12 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
34366
34467
|
}
|
|
34367
34468
|
});
|
|
34368
34469
|
const renew = Effect.fnUntraced(function* (bound) {
|
|
34369
|
-
const client = yield* clientOf(bound.token);
|
|
34370
34470
|
for (;;) {
|
|
34371
|
-
const
|
|
34471
|
+
const key = yield* newRequestId;
|
|
34472
|
+
const renewed = yield* clientOf(bound.kept).pipe(Effect.flatMap((client) => client.sessions.renew({
|
|
34372
34473
|
params: { session: bound.session },
|
|
34373
|
-
headers: { "idempotency-key":
|
|
34374
|
-
}).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"));
|
|
34375
34476
|
if (renewed === "ended") {
|
|
34376
34477
|
yield* oneAtATime.withPermits(1)(Ref.set(state, {
|
|
34377
34478
|
kind: "ended",
|
|
@@ -34383,9 +34484,9 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
34383
34484
|
}
|
|
34384
34485
|
});
|
|
34385
34486
|
const keptThread = (binding) => keychain.get(threadAccount(binding)).pipe(Effect.map(Option.flatMap((kept) => decodeWrittenThread(Redacted.value(kept)))), Effect.orElseSucceed(() => Option.none()));
|
|
34386
|
-
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));
|
|
34387
34488
|
const heldFor = Effect.fnUntraced(function* (binding, over) {
|
|
34388
|
-
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())));
|
|
34389
34490
|
if (!over || Option.isNone(kept) || !(yield* hasEnded(kept.value))) return kept;
|
|
34390
34491
|
return Option.none();
|
|
34391
34492
|
});
|
|
@@ -34424,8 +34525,7 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
34424
34525
|
"ProjectNotFound",
|
|
34425
34526
|
"NotPermitted"
|
|
34426
34527
|
], (refused) => Option.isSome(before) ? Effect.andThen(keychain.remove(threadAccount(binding)).pipe(Effect.ignore), startOn(start.thread)) : Effect.fail(refused)), Effect.orElseSucceed(() => Option.none()));
|
|
34427
|
-
|
|
34428
|
-
return yield* keychain.get(sessionAccount(binding)).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
34528
|
+
return Option.map(started, (one) => one.kept);
|
|
34429
34529
|
});
|
|
34430
34530
|
const startedFor = Effect.fnUntraced(function* (binding, hook, starting) {
|
|
34431
34531
|
const sent = yield* Ref.get(onTheWay);
|
|
@@ -34451,28 +34551,19 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
34451
34551
|
if ((over && !starting || waiting) && !sent) return Option.none();
|
|
34452
34552
|
const binding = yield* bindingForServer(hook.harnessSession);
|
|
34453
34553
|
const held = Option.isNone(binding) ? Option.none() : yield* heldFor(binding.value, over);
|
|
34454
|
-
const
|
|
34455
|
-
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)) {
|
|
34456
34556
|
if (!over) yield* Ref.set(state, {
|
|
34457
34557
|
kind: "unbound",
|
|
34458
34558
|
until: now + unboundMillis
|
|
34459
34559
|
});
|
|
34460
34560
|
return Option.none();
|
|
34461
34561
|
}
|
|
34462
|
-
const
|
|
34463
|
-
const me = yield* client.me.get();
|
|
34464
|
-
if (me.session === void 0) {
|
|
34465
|
-
yield* Ref.set(state, {
|
|
34466
|
-
kind: "unbound",
|
|
34467
|
-
until: now + unboundMillis
|
|
34468
|
-
});
|
|
34469
|
-
return Option.none();
|
|
34470
|
-
}
|
|
34471
|
-
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 } });
|
|
34472
34563
|
const bound = {
|
|
34473
34564
|
binding: binding.value,
|
|
34474
|
-
|
|
34475
|
-
session:
|
|
34565
|
+
kept: kept.value,
|
|
34566
|
+
session: kept.value.session,
|
|
34476
34567
|
generation: registered.generation,
|
|
34477
34568
|
counter: 0,
|
|
34478
34569
|
unreported: []
|
|
@@ -34857,7 +34948,6 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
|
|
|
34857
34948
|
*/
|
|
34858
34949
|
static layerFor(agent) {
|
|
34859
34950
|
return Layer.effect(HarnessSession, Effect.gen(function* () {
|
|
34860
|
-
const config = yield* Ref0Config;
|
|
34861
34951
|
const context = yield* Effect.context();
|
|
34862
34952
|
const state = yield* Ref.make({
|
|
34863
34953
|
kind: "unbound",
|
|
@@ -34877,13 +34967,13 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
|
|
|
34877
34967
|
within: Effect.suspend(() => binding.within),
|
|
34878
34968
|
scope
|
|
34879
34969
|
});
|
|
34880
|
-
const clientOf = (
|
|
34970
|
+
const clientOf = (kept) => keptClient(kept).pipe(Effect.provide(context));
|
|
34881
34971
|
const watch = yield* baseWatch({
|
|
34882
34972
|
scope,
|
|
34883
34973
|
clientOf
|
|
34884
34974
|
});
|
|
34885
34975
|
const settle = Effect.fnUntraced(function* (bound) {
|
|
34886
|
-
const client = yield* clientOf(bound.
|
|
34976
|
+
const client = yield* clientOf(bound.kept);
|
|
34887
34977
|
const refused = () => Effect.succeed("refused");
|
|
34888
34978
|
yield* attribution.settle(ownerOf(bound), (repo, part) => client.sessions.commit({
|
|
34889
34979
|
params: {
|
|
@@ -34929,7 +35019,7 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
|
|
|
34929
35019
|
if (hook.event === "UserPromptSubmit") yield* Ref.set(prompted, Option.some(hook));
|
|
34930
35020
|
const bound = yield* binding.bind(hook);
|
|
34931
35021
|
if (Option.isNone(bound)) return Option.none();
|
|
34932
|
-
const client = yield* clientOf(bound.value.
|
|
35022
|
+
const client = yield* clientOf(bound.value.kept);
|
|
34933
35023
|
const prompt = Option.filter(yield* Ref.get(prompted), (one) => one.harnessSession === hook.harnessSession && one.harnessTurn === hook.harnessTurn);
|
|
34934
35024
|
if (Option.isSome(prompt)) yield* Ref.set(prompted, Option.none());
|
|
34935
35025
|
if (Option.isSome(prompt) && prompt.value !== hook) yield* turns.trackLate(client, bound.value, prompt.value, deadline);
|
|
@@ -35158,11 +35248,6 @@ const lastRead = Effect.fnUntraced(function* (client, session, harness, transcri
|
|
|
35158
35248
|
}
|
|
35159
35249
|
});
|
|
35160
35250
|
});
|
|
35161
|
-
/** The typed `/v1` as a session, with its own token: not the one this process's environment names. */
|
|
35162
|
-
const asSession = (token) => Effect.gen(function* () {
|
|
35163
|
-
const config = yield* Ref0Config;
|
|
35164
|
-
return yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)));
|
|
35165
|
-
});
|
|
35166
35251
|
/** What the hook does, from the hook's input on stdin. */
|
|
35167
35252
|
const endHarnessSession = Effect.gen(function* () {
|
|
35168
35253
|
const stdio = yield* Stdio.Stdio;
|
|
@@ -35174,13 +35259,13 @@ const endHarnessSession = Effect.gen(function* () {
|
|
|
35174
35259
|
const binding = yield* bindingHere(hook.value.session_id);
|
|
35175
35260
|
if (Option.isNone(binding)) return;
|
|
35176
35261
|
const account = sessionAccount(binding.value);
|
|
35177
|
-
const
|
|
35178
|
-
if (Option.isNone(
|
|
35262
|
+
const held = yield* keychain.get(account);
|
|
35263
|
+
if (Option.isNone(held)) return;
|
|
35179
35264
|
yield* Effect.gen(function* () {
|
|
35180
|
-
const
|
|
35181
|
-
|
|
35182
|
-
|
|
35183
|
-
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;
|
|
35184
35269
|
const harness = binding.value.harness === "codex" ? "codex" : "claude-code";
|
|
35185
35270
|
yield* lastRead(client, session, harness, hook.value.transcript_path).pipe(Effect.timeout(lastReadWithin), Effect.ignore);
|
|
35186
35271
|
yield* client.sessions.end({
|
|
@@ -35377,23 +35462,19 @@ const setCommitEnvironment = Effect.fnUntraced(function* (session) {
|
|
|
35377
35462
|
*/
|
|
35378
35463
|
const decodeHook = Schema.decodeUnknownOption(Schema.fromJsonString(SessionStartHookInput));
|
|
35379
35464
|
/**
|
|
35380
|
-
* What blocks the session's thread, in the words its agent reads, read
|
|
35381
|
-
*
|
|
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.
|
|
35382
35467
|
*/
|
|
35383
|
-
const blockedBy = Effect.fnUntraced(function* (
|
|
35384
|
-
const
|
|
35385
|
-
|
|
35386
|
-
const session = Option.isSome(known) ? known.value : (yield* client.me.get()).session?.session;
|
|
35387
|
-
if (session === void 0) return Option.none();
|
|
35388
|
-
const read = yield* client.sessions.blockers({
|
|
35389
|
-
params: { session },
|
|
35468
|
+
const blockedBy = Effect.fnUntraced(function* (kept) {
|
|
35469
|
+
const read = yield* (yield* keptClient(kept)).sessions.blockers({
|
|
35470
|
+
params: { session: kept.session },
|
|
35390
35471
|
query: {}
|
|
35391
35472
|
});
|
|
35392
35473
|
if (read.blockers.length === 0) return Option.none();
|
|
35393
35474
|
return Option.some(renderBlockedBy(read.thread, read.blockers));
|
|
35394
35475
|
});
|
|
35395
35476
|
/** Says what blocks the session's thread, if anything does; says nothing if ref0 can't tell. */
|
|
35396
|
-
const sayBlockers = (
|
|
35477
|
+
const sayBlockers = (kept) => blockedBy(kept).pipe(Effect.flatMap(Option.match({
|
|
35397
35478
|
onNone: () => Effect.void,
|
|
35398
35479
|
onSome: Console.log
|
|
35399
35480
|
})), Effect.ignore);
|
|
@@ -35434,9 +35515,8 @@ const startOnHook = (protocol) => Effect.gen(function* () {
|
|
|
35434
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`;
|
|
35435
35516
|
return yield* Console.log(`ref0: ${why}, so no session starts. In your own terminal, run: ${repair}`);
|
|
35436
35517
|
}
|
|
35437
|
-
const
|
|
35438
|
-
|
|
35439
|
-
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);
|
|
35440
35520
|
const place = startPlace(yield* checkout.threadAt(hook.value.cwd ?? "."), yield* watchedHere);
|
|
35441
35521
|
if (Option.isNone(place.thread)) return;
|
|
35442
35522
|
const thread = place.thread.value;
|
|
@@ -35454,11 +35534,10 @@ const startOnHook = (protocol) => Effect.gen(function* () {
|
|
|
35454
35534
|
version
|
|
35455
35535
|
}).pipe(Effect.tapError((error) => place.trigger.kind === "event" ? Console.log(refusedWords(thread, "nextStep" in error ? error.nextStep : void 0)) : Effect.void));
|
|
35456
35536
|
if (Option.isNone(started)) return;
|
|
35457
|
-
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}.`);
|
|
35458
35538
|
const watched = watchedWords(place, thread);
|
|
35459
35539
|
if (watched !== void 0) yield* Console.log(`ref0: ${watched}`);
|
|
35460
|
-
|
|
35461
|
-
if (Option.isSome(token)) yield* sayBlockers(token.value, Option.some(started.value.session));
|
|
35540
|
+
yield* sayBlockers(started.value.kept);
|
|
35462
35541
|
}).pipe(Effect.timeout("10 seconds"), Effect.ignore);
|
|
35463
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"));
|
|
35464
35543
|
/** The commands a harness runs: `ref0 mcp`, and its command hooks. */
|
|
@@ -35466,7 +35545,7 @@ const harnessCommands = [mcp, Command.make(harnessCommandNames.hook).pipe(Comman
|
|
|
35466
35545
|
//#endregion
|
|
35467
35546
|
//#region src/harness/command-token.ts
|
|
35468
35547
|
/**
|
|
35469
|
-
* 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
|
|
35470
35549
|
* binding. Outside a harness there's none, and a command is its person's, asked by approval; so
|
|
35471
35550
|
* too in a harness whose config folder has no agent connected. Where one is connected, a harness
|
|
35472
35551
|
* session with no token has lost its start, and the command says why, as far as ref0 can tell,
|
|
@@ -35514,11 +35593,12 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
|
|
|
35514
35593
|
const current = Effect.gen(function* () {
|
|
35515
35594
|
const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
|
|
35516
35595
|
if (Option.isNone(binding)) return Option.none();
|
|
35517
|
-
const
|
|
35596
|
+
const held = yield* keychain.get(sessionAccount(binding.value));
|
|
35597
|
+
const kept = readKeptSession(held);
|
|
35518
35598
|
if (Option.isSome(kept)) return kept;
|
|
35519
35599
|
const name = binding.value.harness === "codex" ? "codex" : "claude-code";
|
|
35520
35600
|
const grant = yield* hostGrant.get(yield* folders.folder(name)).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
|
|
35521
|
-
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();
|
|
35522
35602
|
const pending = yield* keychain.get(pendingAccount(binding.value));
|
|
35523
35603
|
const serverRuns = yield* servers.runsUnder(binding.value.process);
|
|
35524
35604
|
return yield* new NoSession({ nextStep: noSessionStep(name, `plugin:${plugin}:${plugin}`, {
|
|
@@ -35714,12 +35794,10 @@ const whoIs = (client) => client.me.get().pipe(Effect.timeout("10 seconds"), Eff
|
|
|
35714
35794
|
InvalidDpopProof: refused,
|
|
35715
35795
|
KeyStoreUnavailable: unsigned
|
|
35716
35796
|
}), Effect.orElseSucceed(() => ({ kind: "unanswered" })));
|
|
35717
|
-
/** A session's token, as a bearer token: sessions aren't bound to a key yet. */
|
|
35718
|
-
const sessionClient = (token) => Effect.flatMap(Ref0Config, (config) => ref0Client(config.api).pipe(Effect.provide(bearerCredential(token))));
|
|
35719
35797
|
const unansweredWords = (api) => `ref0 didn’t answer at ${api}.`;
|
|
35720
35798
|
/** Check the grant kept for this folder, without borrowing another folder's authority. */
|
|
35721
35799
|
const grantChecks = Effect.fnUntraced(function* (grant, configFolder) {
|
|
35722
|
-
const asked = yield*
|
|
35800
|
+
const asked = yield* keptClient(grant).pipe(Effect.flatMap(whoIs), Effect.catchTags({
|
|
35723
35801
|
InvalidDpopProof: refused,
|
|
35724
35802
|
KeychainError: unsigned
|
|
35725
35803
|
}));
|
|
@@ -35819,8 +35897,12 @@ const sessionCheck = Effect.fnUntraced(function* (harness, configFolder) {
|
|
|
35819
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);
|
|
35820
35898
|
const token = yield* Effect.result((yield* Keychain).get(sessionAccount(binding.value)));
|
|
35821
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);
|
|
35822
|
-
|
|
35823
|
-
|
|
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
|
+
}));
|
|
35824
35906
|
switch (asked.kind) {
|
|
35825
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}`);
|
|
35826
35908
|
case "refused": return checkOf("session", "no", "ref0 no longer follows this session", launch);
|
|
@@ -35918,6 +36000,7 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
|
|
|
35918
36000
|
]));
|
|
35919
36001
|
const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
|
|
35920
36002
|
const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
|
|
36003
|
+
const machineKey = MachineKey.layer.pipe(Layer.provide(keychain));
|
|
35921
36004
|
const sessionToken = harnessSessionToken.pipe(Layer.provide([
|
|
35922
36005
|
keychain,
|
|
35923
36006
|
harnessProcess,
|
|
@@ -35928,8 +36011,15 @@ const sessionToken = harnessSessionToken.pipe(Layer.provide([
|
|
|
35928
36011
|
const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
|
|
35929
36012
|
Ref0Config.layer,
|
|
35930
36013
|
sessionToken,
|
|
35931
|
-
FetchHttpClient.layer
|
|
35932
|
-
|
|
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));
|
|
35933
36023
|
//#endregion
|
|
35934
36024
|
//#region src/root.ts
|
|
35935
36025
|
/**
|