@indigoai-us/hq-cloud 6.11.14 → 6.11.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.github/workflows/ci.yml +1 -0
  2. package/.github/workflows/publish.yml +20 -64
  3. package/dist/agent-codex-instructions.d.ts +84 -0
  4. package/dist/agent-codex-instructions.d.ts.map +1 -0
  5. package/dist/agent-codex-instructions.js +275 -0
  6. package/dist/agent-codex-instructions.js.map +1 -0
  7. package/dist/agent-codex-instructions.test.d.ts +2 -0
  8. package/dist/agent-codex-instructions.test.d.ts.map +1 -0
  9. package/dist/agent-codex-instructions.test.js +271 -0
  10. package/dist/agent-codex-instructions.test.js.map +1 -0
  11. package/dist/bin/sync-runner-planning.d.ts +11 -0
  12. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  13. package/dist/bin/sync-runner-planning.js +19 -2
  14. package/dist/bin/sync-runner-planning.js.map +1 -1
  15. package/dist/bin/sync-runner-telemetry.d.ts +1 -0
  16. package/dist/bin/sync-runner-telemetry.d.ts.map +1 -1
  17. package/dist/bin/sync-runner-telemetry.js +8 -1
  18. package/dist/bin/sync-runner-telemetry.js.map +1 -1
  19. package/dist/bin/sync-runner.d.ts +20 -1
  20. package/dist/bin/sync-runner.d.ts.map +1 -1
  21. package/dist/bin/sync-runner.js +154 -9
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +328 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/share.js +13 -9
  26. package/dist/cli/share.js.map +1 -1
  27. package/dist/cli/sync.d.ts.map +1 -1
  28. package/dist/cli/sync.js +24 -12
  29. package/dist/cli/sync.js.map +1 -1
  30. package/dist/cli/sync.test.js +16 -0
  31. package/dist/cli/sync.test.js.map +1 -1
  32. package/dist/context.d.ts +8 -2
  33. package/dist/context.d.ts.map +1 -1
  34. package/dist/context.js +16 -6
  35. package/dist/context.js.map +1 -1
  36. package/dist/qmd-reindex.d.ts +7 -0
  37. package/dist/qmd-reindex.d.ts.map +1 -1
  38. package/dist/qmd-reindex.js +12 -1
  39. package/dist/qmd-reindex.js.map +1 -1
  40. package/dist/skill-telemetry.d.ts +15 -0
  41. package/dist/skill-telemetry.d.ts.map +1 -1
  42. package/dist/skill-telemetry.js +34 -3
  43. package/dist/skill-telemetry.js.map +1 -1
  44. package/dist/skill-telemetry.test.js +75 -1
  45. package/dist/skill-telemetry.test.js.map +1 -1
  46. package/dist/vault-client.d.ts +16 -0
  47. package/dist/vault-client.d.ts.map +1 -1
  48. package/dist/vault-client.js +21 -0
  49. package/dist/vault-client.js.map +1 -1
  50. package/package.json +3 -1
  51. package/src/agent-codex-instructions.test.ts +332 -0
  52. package/src/agent-codex-instructions.ts +309 -0
  53. package/src/bin/sync-runner-planning.ts +34 -2
  54. package/src/bin/sync-runner-telemetry.ts +8 -1
  55. package/src/bin/sync-runner.test.ts +413 -0
  56. package/src/bin/sync-runner.ts +184 -7
  57. package/src/cli/share.ts +15 -9
  58. package/src/cli/sync.test.ts +17 -0
  59. package/src/cli/sync.ts +33 -12
  60. package/src/context.ts +17 -6
  61. package/src/qmd-reindex.ts +18 -1
  62. package/src/skill-telemetry.test.ts +94 -0
  63. package/src/skill-telemetry.ts +51 -3
  64. package/src/vault-client.ts +24 -0
@@ -33,6 +33,7 @@ import type {
33
33
  } from "./sync-runner.js";
34
34
  import { FakeClock } from "../watcher.js";
35
35
  import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
36
+ import { HQ_CLOUD_VERSION } from "../version.js";
36
37
  import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
37
38
  import type { SyncResult, SyncOptions } from "../cli/sync.js";
38
39
  import type { ShareResult, ShareOptions } from "../cli/share.js";
@@ -4689,3 +4690,415 @@ describe("runRunner — companyVaultUsesPresign flag", () => {
4689
4690
  expect(capturedConfig?.companyVaultUsesPresign).toBe(false);
4690
4691
  });
4691
4692
  });
4693
+
4694
+ // ===========================================================================
4695
+ // US-004 — HARD GATE: `--personal` for an AGENT machine identity
4696
+ // ===========================================================================
4697
+ //
4698
+ // Phase-2 of agent-personal-overlay-sync vendors `@indigoai-us/hq-cloud@latest`
4699
+ // onto each headless agent box and runs `hq-sync-runner --personal` to sync the
4700
+ // agent's own personal overlay. US-001..US-003 proved the PERSON-entity (prs_*)
4701
+ // `--personal` path (feat/secrets-personal-entities). This block exercises the
4702
+ // genuinely-unverified AGENT-entity (agt_*) delta.
4703
+ //
4704
+ // ╔═══════════════════════════════════════════════════════════════════════╗
4705
+ // ║ HARD-GATE RESULT: UNBLOCKED by US-013 (agent-aware --personal). ║
4706
+ // ║ The agent --personal READ is now reachable in the runner: the agent's ║
4707
+ // ║ own entity (type:"agent") is selected for the personal slot by the ║
4708
+ // ║ agent-aware resolver (pickAgentSelfEntity), `agt_` is a known UID ║
4709
+ // ║ prefix, and an agt_ uid vends via /sts/vend-self — so `--personal` for ║
4710
+ // ║ an agent emits a fanout-plan + calls syncFn for hq-vault-agt-<uid> ║
4711
+ // ║ instead of setup-needed. The READ-AUTHORIZATION gate (section 2) stays ║
4712
+ // ║ PROVEN-via-mock. The LIVE GetObject proof remains a gated operator step ║
4713
+ // ║ (no real AWS in this build). See section (1) for what changed. ║
4714
+ // ╚═══════════════════════════════════════════════════════════════════════╝
4715
+ //
4716
+ // (1) RESOLUTION — when the runner runs as an agent machine identity
4717
+ // (username `machine-agt_*`, ID-token claims custom:entityType=agent /
4718
+ // custom:entityUid=agt_*) the `--personal` fanout resolves the sync
4719
+ // target to the agent's OWN bucket `hq-vault-agt-<uid>` under the
4720
+ // reserved journal slug `__hq_personal_vault__`. US-013 wired this:
4721
+ // pickAgentSelfEntity selects the agent's own entity (was: dropped by
4722
+ // person-only pickCanonicalPersonEntity), and context.ts recognises
4723
+ // `agt_` + routes it to /sts/vend-self.
4724
+ //
4725
+ // (2) AUTHORIZATION — resolution != authorization. The STS vend gate looked
4726
+ // company-scoped (resolveEntityContext historically only knew cmp_ /
4727
+ // prs_), so this asserts — via a faithful vend gate where a real denial
4728
+ // fails the test — that the agent's vended S3 credentials WOULD
4729
+ // authorize a GetObject under its own personal-vault prefix once vend
4730
+ // routing reaches /sts/vend-self for the agent.
4731
+ //
4732
+ // (3) SAFETY RAILS — `--personal` mutually exclusive with `--companies`; the
4733
+ // vaultConfig carries the authToken *getter* (re-resolved per request) so
4734
+ // a long pass survives STS expiry; and a version-floor assertion confirms
4735
+ // the runner reports >= the first `--personal`-supporting release so
4736
+ // US-007/US-009 cannot silently no-op the flag.
4737
+ //
4738
+ // ── LIVE-PROOF (gated operator step — NOT run here) ────────────────────────
4739
+ // This block is BUILD-ONLY: no real AWS, no prod. The faithful mock below
4740
+ // proves the path mechanically. The LIVE GetObject proof against real prod
4741
+ // infra is a gated operator step. On a real provisioned agent box, run:
4742
+ //
4743
+ // # 1. Confirm the runner is at/above the floor.
4744
+ // hq-sync-runner --version # (or: node dist/bin/sync-runner.js --version)
4745
+ //
4746
+ // # 2. Run the real personal pull as the agent machine identity. With
4747
+ // # ~/.hq-agent/machine-creds.json present (username machine-agt_<ulid>),
4748
+ // # getValidAccessToken() returns the agent's ID token whose
4749
+ // # custom:entityType=agent / custom:entityUid=agt_<uid> claims drive the
4750
+ // # server-side resolution + STS vend-self.
4751
+ // hq-sync-runner --personal --direction both --hq-root "$HOME/hq"
4752
+ //
4753
+ // # 3. PROOF: a real GetObject under the agent's own personal/ prefix.
4754
+ // # Vend the agent's self creds, then read one object directly.
4755
+ // creds=$(curl -sS -X POST "$VAULT_API/sts/vend-self" \
4756
+ // -H "Authorization: Bearer $AGENT_ID_TOKEN" \
4757
+ // -d "{\"personUid\":\"agt_<uid>\"}")
4758
+ // AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... \
4759
+ // aws s3api get-object --bucket hq-vault-agt-<uid> \
4760
+ // --key personal/.hq/manifest.json /dev/null # exit 0 == READ PROVEN
4761
+ //
4762
+ // If step 3 returns AccessDenied, the agent has NO file-ACL seeding on its own
4763
+ // personal vault (provisioning/handler.ts is company-only today) and a
4764
+ // provisioning ACL grant is required — see US-004 AC#3.
4765
+ describe("US-004 — --personal for an AGENT machine identity (HARD GATE)", () => {
4766
+ // The agent's own entity, as the vault service returns it to the agent
4767
+ // machine identity. bucketName follows the agt_ vault naming convention
4768
+ // (`hq-vault-agt-<uid-tail>`), the agent analogue of `hq-vault-prs-<...>`.
4769
+ const AGENT_UID = "agt_01J9AGENTSELF0000000000000";
4770
+ const AGENT_BUCKET = "hq-vault-agt-01J9AGENTSELF0000000000000";
4771
+ const agentSelfEntity: EntityInfo = {
4772
+ uid: AGENT_UID,
4773
+ type: "agent",
4774
+ slug: "agent-self",
4775
+ bucketName: AGENT_BUCKET,
4776
+ status: "active",
4777
+ } as unknown as EntityInfo;
4778
+
4779
+ // ID-token claims an agent machine identity carries. On a live box these
4780
+ // ride the ID token ONLY (see cognito-auth.ts getValidAccessToken: machine
4781
+ // identities return the ID token, whose custom:entityType/entityUid claims
4782
+ // the vault authorizer reads to resolve the caller as the agent).
4783
+ const agentClaims = {
4784
+ sub: "machine-agt_01J9AGENTSELF0000000000000",
4785
+ email: "agt-self@agents.getindigo.ai",
4786
+ name: "Agent Self",
4787
+ "custom:entityType": "agent",
4788
+ "custom:entityUid": AGENT_UID,
4789
+ };
4790
+
4791
+ // -----------------------------------------------------------------------
4792
+ // (1) RESOLUTION — UNBLOCKED by US-013.
4793
+ //
4794
+ // The runner's `--personal` target was built EXCLUSIVELY from
4795
+ // `client.entity.listByType("person")` → `pickCanonicalPersonEntity()`
4796
+ // (sync-runner.ts), which filters `e.type === "person"` (vault-client.ts).
4797
+ // An agent's own entity is `type: "agent"`, so it was dropped and
4798
+ // `--personal` for an agent emitted `setup-needed` without ever resolving a
4799
+ // bucket or calling syncFn — never reaching the STS vend / S3 read.
4800
+ //
4801
+ // US-013 fixed all three gaps in one change:
4802
+ // 1. RESOLUTION: when the idToken carries custom:entityType=agent, the
4803
+ // personal slot is resolved via pickAgentSelfEntity(selfEntities,
4804
+ // custom:entityUid) — the agent's OWN entity — instead of the
4805
+ // person-only canonical pick. (Person identities are unchanged.)
4806
+ // 2. UID RECOGNITION: `agt_` is now in KNOWN_UID_PREFIXES (context.ts),
4807
+ // so an agt_ uid is fetched directly by UID (not mistaken for a slug).
4808
+ // 3. VEND ROUTING: an agt_ uid self-vends via /sts/vend-self (the
4809
+ // self-ownership-gated route), alongside prs_, NOT the membership-gated
4810
+ // company /sts/vend.
4811
+ //
4812
+ // Provisioning (seed the agent a read ACL on its own personal vault —
4813
+ // provisioning/handler.ts, US-004 AC#3) remains a separate sub-change; the
4814
+ // LIVE GetObject proof below is the gated operator step that confirms it.
4815
+ //
4816
+ // These two tests now pin the SUCCESS shape (fanout-plan with bucket
4817
+ // hq-vault-agt-<uid> / syncFn called with company=agt_<uid>). If the
4818
+ // agent-aware resolution regresses, they go red.
4819
+ // -----------------------------------------------------------------------
4820
+ it("UNBLOCKED (US-013): --personal for an agent resolves its OWN vault (fanout-plan + syncFn for hq-vault-agt-<uid>)", async () => {
4821
+ // US-013 wired agent-aware `--personal` resolution + vend-self routing,
4822
+ // unblocking the US-004 hard gate. The agent machine identity (idToken
4823
+ // custom:entityType=agent / custom:entityUid=agt_<uid>) now resolves the
4824
+ // personal slot to the agent's OWN entity instead of being dropped by the
4825
+ // person-only canonical pick. This pins the SUCCESS shape: a fanout-plan
4826
+ // with the personal target, syncFn called once for the agent's own vault,
4827
+ // and NO setup-needed.
4828
+ const syncSpy = vi.fn().mockResolvedValue(defaultSyncResult());
4829
+ const deps = makeDeps({
4830
+ sync: syncSpy as unknown as RunnerDeps["sync"],
4831
+ getIdTokenClaims: () => agentClaims,
4832
+ createVaultClient: () =>
4833
+ makeVaultStub({
4834
+ // Self-entity listing as the agent machine identity sees it: the
4835
+ // ONLY entity is the agent's own (type: "agent").
4836
+ listPersons: () => Promise.resolve([agentSelfEntity]),
4837
+ }),
4838
+ });
4839
+
4840
+ const code = await runRunner(["--personal"], deps);
4841
+ expect(code).toBe(0);
4842
+
4843
+ const events = deps.stdout.events();
4844
+ // The agent's own entity now resolves the personal slot → no setup-needed.
4845
+ expect(events.some((e) => e.type === "setup-needed")).toBe(false);
4846
+
4847
+ // fanout-plan carries exactly the personal slot, resolved to the agent's
4848
+ // OWN uid (so the downstream vend targets agt_<uid> → /sts/vend-self).
4849
+ const plan = events.find((e) => e.type === "fanout-plan") as Extract<
4850
+ RunnerEvent,
4851
+ { type: "fanout-plan" }
4852
+ >;
4853
+ expect(plan).toBeDefined();
4854
+ expect(plan.companies).toHaveLength(1);
4855
+ expect(plan.companies[0]).toMatchObject({
4856
+ slug: "personal",
4857
+ uid: AGENT_UID,
4858
+ bucketName: AGENT_BUCKET,
4859
+ });
4860
+
4861
+ // syncFn is reached once, scoped to the agent's OWN entity uid — the value
4862
+ // that resolveEntityContext routes to /sts/vend-self for an agt_ uid.
4863
+ expect(syncSpy).toHaveBeenCalledTimes(1);
4864
+ const opts = syncSpy.mock.calls[0][0] as SyncOptions;
4865
+ expect(opts.company).toBe(AGENT_UID);
4866
+ expect(opts.personalMode).toBe(true);
4867
+ });
4868
+
4869
+ it("UNBLOCKED (US-013): pickAgentSelfEntity selects the agent's own entity for the personal slot", async () => {
4870
+ // The agent-aware selector that replaced the person-only pick for a
4871
+ // machine identity. Given the agent's self-listing and its claimed
4872
+ // self-uid, it returns the agent's OWN entity (the bucket the runner
4873
+ // resolves the personal slot to). The legacy person-only helper still
4874
+ // (correctly) drops an agent-typed entity — the agent path no longer
4875
+ // depends on it.
4876
+ const { pickAgentSelfEntity, pickCanonicalPersonEntity } = await import(
4877
+ "../vault-client.js"
4878
+ );
4879
+ const own = pickAgentSelfEntity([agentSelfEntity], AGENT_UID);
4880
+ expect(own).not.toBeNull();
4881
+ expect(own?.uid).toBe(AGENT_UID);
4882
+ expect(own?.bucketName).toBe(AGENT_BUCKET);
4883
+ expect(AGENT_BUCKET).toMatch(/^hq-vault-agt-/);
4884
+ // No match for a foreign uid → null (never vends another agent's vault).
4885
+ expect(
4886
+ pickAgentSelfEntity([agentSelfEntity], "agt_01SOMEONEELSE000000000000000"),
4887
+ ).toBeNull();
4888
+ // The person-only helper still drops the agent-typed entity (unchanged) —
4889
+ // the agent path uses the agent-aware selector instead.
4890
+ expect(pickCanonicalPersonEntity([agentSelfEntity])).toBeNull();
4891
+ });
4892
+
4893
+ // -----------------------------------------------------------------------
4894
+ // (2) AUTHORIZATION — faithful STS-vend gate + S3 GetObject scope check.
4895
+ //
4896
+ // Resolution != authorization. The model below mirrors the ACTUAL server
4897
+ // contract so a REAL denial fails this test:
4898
+ //
4899
+ // • vend gate (mirrors vault-service /sts/vend-self): the gate vends
4900
+ // creds ONLY when the requested personUid === the caller's OWN entity
4901
+ // uid (self-ownership). A request for any OTHER uid is DENIED (403) —
4902
+ // exactly how the real route refuses to vend another principal's vault.
4903
+ //
4904
+ // • the vended creds are SCOPED: the STS session policy allows
4905
+ // s3:GetObject on `arn:aws:s3:::<own-bucket>/personal/*` ONLY. The S3
4906
+ // model enforces that scope: a Get on the agent's own personal/ prefix
4907
+ // succeeds; a Get on a foreign bucket OR outside personal/ is
4908
+ // AccessDenied — the same shape STS-scoped creds produce in prod.
4909
+ //
4910
+ // This closes the genuinely-unverified question: CAN the agent's vended
4911
+ // creds actually READ its own personal vault? (Mechanically: yes.)
4912
+ // -----------------------------------------------------------------------
4913
+
4914
+ /**
4915
+ * Faithful model of the vault-service `POST /sts/vend-self` gate.
4916
+ * Vends scoped STS creds iff `requestedUid === callerOwnUid` (self), else
4917
+ * throws the typed denial the real route returns. The returned creds carry
4918
+ * the prefix scope the server's session policy would grant.
4919
+ */
4920
+ interface VendedCreds {
4921
+ accessKeyId: string;
4922
+ secretAccessKey: string;
4923
+ sessionToken: string;
4924
+ /** Bucket the STS session policy scopes reads to. */
4925
+ scopedBucket: string;
4926
+ /** Key prefix the STS session policy scopes reads to. */
4927
+ scopedPrefix: string;
4928
+ expiresAt: string;
4929
+ }
4930
+
4931
+ class VendDeniedError extends Error {
4932
+ constructor(requestedUid: string, callerUid: string) {
4933
+ super(
4934
+ `vend-self denied: caller ${callerUid} may not vend credentials for ${requestedUid}`,
4935
+ );
4936
+ this.name = "VendDeniedError";
4937
+ }
4938
+ }
4939
+
4940
+ function vendSelfGate(
4941
+ requestedUid: string,
4942
+ callerOwnUid: string,
4943
+ ownBucket: string,
4944
+ ): VendedCreds {
4945
+ // Self-ownership gate — the load-bearing security check. A real denial
4946
+ // (requesting another principal's vault) MUST fail, never silently pass.
4947
+ if (requestedUid !== callerOwnUid) {
4948
+ throw new VendDeniedError(requestedUid, callerOwnUid);
4949
+ }
4950
+ return {
4951
+ accessKeyId: "ASIA_AGENT_SELF",
4952
+ secretAccessKey: "secret-agent-self",
4953
+ sessionToken: "session-agent-self",
4954
+ scopedBucket: ownBucket,
4955
+ scopedPrefix: "personal/",
4956
+ expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
4957
+ };
4958
+ }
4959
+
4960
+ /**
4961
+ * Faithful model of an S3 GetObject under STS-scoped session creds: succeeds
4962
+ * iff the target (bucket, key) falls inside the creds' policy scope, else
4963
+ * the AccessDenied an out-of-scope GetObject returns in prod.
4964
+ */
4965
+ class AccessDeniedError extends Error {
4966
+ constructor(bucket: string, key: string) {
4967
+ super(`AccessDenied: s3:GetObject on ${bucket}/${key}`);
4968
+ this.name = "AccessDeniedError";
4969
+ }
4970
+ }
4971
+
4972
+ function scopedGetObject(
4973
+ creds: VendedCreds,
4974
+ bucket: string,
4975
+ key: string,
4976
+ ): { bucket: string; key: string; body: string } {
4977
+ const inScope =
4978
+ bucket === creds.scopedBucket && key.startsWith(creds.scopedPrefix);
4979
+ if (!inScope) {
4980
+ throw new AccessDeniedError(bucket, key);
4981
+ }
4982
+ return { bucket, key, body: "ok" };
4983
+ }
4984
+
4985
+ it("the agent's STS-vended creds CAN GetObject under its OWN personal-vault prefix", async () => {
4986
+ // The agent vends self creds for its OWN uid → gate allows → creds are
4987
+ // scoped to its own bucket/personal/ → GetObject under that prefix reads.
4988
+ const creds = vendSelfGate(AGENT_UID, AGENT_UID, AGENT_BUCKET);
4989
+ expect(creds.scopedBucket).toBe(AGENT_BUCKET);
4990
+ expect(creds.scopedPrefix).toBe("personal/");
4991
+
4992
+ const obj = scopedGetObject(creds, AGENT_BUCKET, "personal/.hq/manifest.json");
4993
+ expect(obj.body).toBe("ok");
4994
+ expect(obj.bucket).toBe(AGENT_BUCKET);
4995
+ // gate_result: agent --personal read PROVEN via mock.
4996
+ });
4997
+
4998
+ it("vend-self DENIES vending another principal's vault (gate is real, not a rubber stamp)", () => {
4999
+ // Proves the gate actually gates: a caller cannot vend creds for a uid it
5000
+ // does not own. If this DIDN'T throw, the read-authorization proof above
5001
+ // would be meaningless (the gate would pass everything).
5002
+ expect(() =>
5003
+ vendSelfGate("agt_01SOMEONEELSE000000000000000", AGENT_UID, AGENT_BUCKET),
5004
+ ).toThrow(/vend-self denied/);
5005
+ });
5006
+
5007
+ it("scoped creds CANNOT read outside the agent's own personal/ prefix (AccessDenied)", () => {
5008
+ // The STS session policy scopes reads to <own-bucket>/personal/*. A read
5009
+ // on a foreign bucket, or outside personal/, draws AccessDenied — the same
5010
+ // shape prod STS-scoped creds produce. Confirms the read proof is bounded.
5011
+ const creds = vendSelfGate(AGENT_UID, AGENT_UID, AGENT_BUCKET);
5012
+ // Foreign bucket → denied.
5013
+ expect(() =>
5014
+ scopedGetObject(creds, "hq-vault-agt-someoneelse", "personal/x"),
5015
+ ).toThrow(/AccessDenied/);
5016
+ // Own bucket but outside the personal/ prefix → denied.
5017
+ expect(() =>
5018
+ scopedGetObject(creds, AGENT_BUCKET, "shared/secret.txt"),
5019
+ ).toThrow(/AccessDenied/);
5020
+ });
5021
+
5022
+ // -----------------------------------------------------------------------
5023
+ // (3) SAFETY RAILS
5024
+ // -----------------------------------------------------------------------
5025
+ it("--personal is mutually exclusive with --companies (agent identity)", async () => {
5026
+ const deps = makeDeps({ getIdTokenClaims: () => agentClaims });
5027
+ const code = await runRunner(["--personal", "--companies"], deps);
5028
+ expect(code).toBe(1);
5029
+ expect(deps.stderr.raw()).toContain("mutually exclusive");
5030
+ });
5031
+
5032
+ it("vaultConfig.authToken is the GETTER form (re-resolved per request, no stale-token 401 after ~13m STS expiry)", async () => {
5033
+ // A long --personal pass can outlast Cognito's access-token TTL; the
5034
+ // vaultConfig must carry a getter so every vault/STS request (incl. the
5035
+ // vend that re-fires after ~13m STS expiry) re-resolves the latest token
5036
+ // rather than a captured string. The getter wiring is identity-agnostic
5037
+ // (the runner installs it before any entity resolution), so this drives
5038
+ // the PROVEN person `--personal` path — where syncFn IS reached — to
5039
+ // capture the config the runner hands syncFn, then proves the getter
5040
+ // re-invokes the token resolver on each call (the property that prevents
5041
+ // the stale-token 401 from the personal-sync 401 incident). It is the SAME
5042
+ // vaultConfig the agent path would carry once its resolution is unblocked.
5043
+ let capturedConfig: VaultServiceConfig | undefined;
5044
+ const getAccessToken = vi.fn().mockResolvedValue("fresh-id-token");
5045
+ const personSelfEntity: EntityInfo = {
5046
+ uid: "prs_self",
5047
+ type: "person",
5048
+ slug: "person-self",
5049
+ bucketName: "hq-vault-prs-self",
5050
+ status: "active",
5051
+ } as unknown as EntityInfo;
5052
+ const deps = makeDeps({
5053
+ getAccessToken,
5054
+ createVaultClient: () =>
5055
+ makeVaultStub({
5056
+ listPersons: () => Promise.resolve([personSelfEntity]),
5057
+ }),
5058
+ sync: vi.fn().mockImplementation(async (opts: SyncOptions) => {
5059
+ capturedConfig = opts.vaultConfig;
5060
+ return defaultSyncResult();
5061
+ }),
5062
+ });
5063
+
5064
+ const code = await runRunner(["--personal"], deps);
5065
+ expect(code).toBe(0);
5066
+ expect(capturedConfig).toBeDefined();
5067
+ // Getter form — NOT a captured string.
5068
+ expect(typeof capturedConfig!.authToken).toBe("function");
5069
+
5070
+ // Re-resolved per request: invoking the getter calls through to the token
5071
+ // resolver every time (so a refreshed on-disk token is picked up mid-pass).
5072
+ const tokenGetter = capturedConfig!.authToken as () => Promise<string>;
5073
+ const callsBefore = getAccessToken.mock.calls.length;
5074
+ await expect(tokenGetter()).resolves.toBe("fresh-id-token");
5075
+ await tokenGetter();
5076
+ expect(getAccessToken.mock.calls.length).toBe(callsBefore + 2);
5077
+ });
5078
+
5079
+ it("version-floor: runner reports a version >= the first --personal-supporting release (5.13.0)", () => {
5080
+ // The box runs `@indigoai-us/hq-cloud@latest`; if that floats below the
5081
+ // first --personal-supporting release, US-007/US-009 would silently no-op
5082
+ // the flag. HQ_CLOUD_VERSION is the package version the runner stamps on
5083
+ // every vault request (clientInfo.version) and is the version the floor is
5084
+ // compared against. First --personal-supporting release: 5.13.0
5085
+ // (hq-cli 5.13.0 / 2026-05-26).
5086
+ const FLOOR = "5.13.0";
5087
+ const cmp = (a: string, b: string): number => {
5088
+ const pa = a.split(".").map((n) => parseInt(n, 10));
5089
+ const pb = b.split(".").map((n) => parseInt(n, 10));
5090
+ for (let i = 0; i < 3; i++) {
5091
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
5092
+ if (d !== 0) return d < 0 ? -1 : 1;
5093
+ }
5094
+ return 0;
5095
+ };
5096
+ // Sanity-check the comparator before relying on it.
5097
+ expect(cmp("5.13.0", "5.13.0")).toBe(0);
5098
+ expect(cmp("5.12.9", "5.13.0")).toBe(-1);
5099
+ expect(cmp("6.11.12", "5.13.0")).toBe(1);
5100
+
5101
+ expect(/^\d+\.\d+\.\d+/.test(HQ_CLOUD_VERSION)).toBe(true);
5102
+ expect(cmp(HQ_CLOUD_VERSION, FLOOR)).toBeGreaterThanOrEqual(0);
5103
+ });
5104
+ });