@indigoai-us/hq-cloud 6.14.19 → 6.14.20
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/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +8 -16
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +138 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +55 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-planning.test.ts +153 -0
- package/src/bin/sync-runner-planning.ts +10 -17
- package/src/bin/sync-runner.test.ts +71 -0
- package/src/bin/sync-runner.ts +15 -2
package/package.json
CHANGED
|
@@ -87,6 +87,159 @@ describe("resolveMembershipsForRun — setup-needed reasons", () => {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
describe("buildFanoutPlan — setup-needed reasons", () => {
|
|
90
|
+
it("resolves an agent machine identity to only its claimed personal vault", async () => {
|
|
91
|
+
const agentUid = "agt_01J9AGENTSELF0000000000000";
|
|
92
|
+
const listByType = vi.fn((type: string) =>
|
|
93
|
+
Promise.resolve(
|
|
94
|
+
type === "agent"
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
uid: agentUid,
|
|
98
|
+
slug: "agent-self",
|
|
99
|
+
type: "agent",
|
|
100
|
+
status: "active",
|
|
101
|
+
bucketName: `hq-vault-${agentUid}`,
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
: [],
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
const result = await buildFanoutPlan({
|
|
108
|
+
memberships: [],
|
|
109
|
+
companies: false,
|
|
110
|
+
personal: true,
|
|
111
|
+
skipPersonal: false,
|
|
112
|
+
client: clientStub({
|
|
113
|
+
entity: { get: () => Promise.resolve(undefined), listByType },
|
|
114
|
+
}),
|
|
115
|
+
claims: {
|
|
116
|
+
"custom:entityType": "agent",
|
|
117
|
+
"custom:entityUid": agentUid,
|
|
118
|
+
},
|
|
119
|
+
resolveSkipPersonal: () => false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(listByType).toHaveBeenCalledWith("agent");
|
|
123
|
+
expect(result).toEqual({
|
|
124
|
+
status: "plan",
|
|
125
|
+
plan: [
|
|
126
|
+
{
|
|
127
|
+
slug: "personal",
|
|
128
|
+
uid: agentUid,
|
|
129
|
+
bucketName: `hq-vault-${agentUid}`,
|
|
130
|
+
personalMode: true,
|
|
131
|
+
journalSlug: "__hq_personal_vault__",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not let an agent machine identity fall back to a person vault", async () => {
|
|
138
|
+
const listByType = vi.fn(() => Promise.resolve([]));
|
|
139
|
+
const result = await buildFanoutPlan({
|
|
140
|
+
memberships: [],
|
|
141
|
+
companies: false,
|
|
142
|
+
personal: true,
|
|
143
|
+
skipPersonal: false,
|
|
144
|
+
client: clientStub({
|
|
145
|
+
entity: { get: () => Promise.resolve(undefined), listByType },
|
|
146
|
+
}),
|
|
147
|
+
claims: {
|
|
148
|
+
"custom:entityType": "agent",
|
|
149
|
+
"custom:entityUid": "agt_01J9AGENTSELF0000000000000",
|
|
150
|
+
},
|
|
151
|
+
resolveSkipPersonal: () => false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(listByType).toHaveBeenCalledWith("agent");
|
|
155
|
+
expect(result).toEqual({ status: "setup-needed", reason: "no-person-entity" });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("folds the same agent personal target into a --companies plan", async () => {
|
|
159
|
+
const agentUid = "agt_01J9AGENTSELF0000000000000";
|
|
160
|
+
const result = await buildFanoutPlan({
|
|
161
|
+
memberships: [{ companyUid: "cmp_team" }],
|
|
162
|
+
companies: true,
|
|
163
|
+
personal: false,
|
|
164
|
+
skipPersonal: false,
|
|
165
|
+
client: clientStub({
|
|
166
|
+
entity: {
|
|
167
|
+
get: () =>
|
|
168
|
+
Promise.resolve({
|
|
169
|
+
uid: "cmp_team",
|
|
170
|
+
slug: "team",
|
|
171
|
+
type: "company",
|
|
172
|
+
status: "active",
|
|
173
|
+
bucketName: "hq-vault-cmp-team",
|
|
174
|
+
}),
|
|
175
|
+
listByType: (type: string) => {
|
|
176
|
+
expect(type).toBe("agent");
|
|
177
|
+
return Promise.resolve([
|
|
178
|
+
{
|
|
179
|
+
uid: agentUid,
|
|
180
|
+
slug: "agent-self",
|
|
181
|
+
type: "agent",
|
|
182
|
+
status: "active",
|
|
183
|
+
bucketName: `hq-vault-${agentUid}`,
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
claims: {
|
|
190
|
+
"custom:entityType": "agent",
|
|
191
|
+
"custom:entityUid": agentUid,
|
|
192
|
+
},
|
|
193
|
+
resolveSkipPersonal: () => false,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result).toMatchObject({
|
|
197
|
+
status: "plan",
|
|
198
|
+
plan: [
|
|
199
|
+
{ uid: "cmp_team", slug: "team" },
|
|
200
|
+
{
|
|
201
|
+
uid: agentUid,
|
|
202
|
+
slug: "personal",
|
|
203
|
+
bucketName: `hq-vault-${agentUid}`,
|
|
204
|
+
personalMode: true,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("keeps the human personal target on the person collection", async () => {
|
|
211
|
+
const personUid = "prs_01J9PERSONSELF000000000000";
|
|
212
|
+
const listByType = vi.fn(() =>
|
|
213
|
+
Promise.resolve([
|
|
214
|
+
{
|
|
215
|
+
uid: personUid,
|
|
216
|
+
slug: "person-self",
|
|
217
|
+
type: "person",
|
|
218
|
+
status: "active",
|
|
219
|
+
bucketName: `hq-vault-${personUid}`,
|
|
220
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
221
|
+
},
|
|
222
|
+
]),
|
|
223
|
+
);
|
|
224
|
+
const result = await buildFanoutPlan({
|
|
225
|
+
memberships: [],
|
|
226
|
+
companies: false,
|
|
227
|
+
personal: true,
|
|
228
|
+
skipPersonal: false,
|
|
229
|
+
client: clientStub({
|
|
230
|
+
entity: { get: () => Promise.resolve(undefined), listByType },
|
|
231
|
+
}),
|
|
232
|
+
claims: { sub: "human-sub" },
|
|
233
|
+
resolveSkipPersonal: () => false,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(listByType).toHaveBeenCalledWith("person");
|
|
237
|
+
expect(result).toMatchObject({
|
|
238
|
+
status: "plan",
|
|
239
|
+
plan: [{ uid: personUid, bucketName: `hq-vault-${personUid}` }],
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
90
243
|
it("reports no-person-entity when --personal has no resolvable personal target", async () => {
|
|
91
244
|
const result = await buildFanoutPlan({
|
|
92
245
|
memberships: [],
|
|
@@ -177,26 +177,19 @@ export async function buildFanoutPlan(options: {
|
|
|
177
177
|
(options.companies || options.personal) &&
|
|
178
178
|
!options.resolveSkipPersonal(options.skipPersonal)
|
|
179
179
|
) {
|
|
180
|
-
// Self-entity listing.
|
|
181
|
-
// person
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// the JWT, so an agent box gets back its own agt_ entity here.
|
|
186
|
-
const persons = await options.client.entity.listByType("person");
|
|
187
|
-
|
|
188
|
-
// Agent-aware target selection (US-013, unblocks US-004): when the runner
|
|
189
|
-
// is an agent machine identity, the person-only canonical pick drops the
|
|
190
|
-
// agent's own `type: "agent"` entity — so resolve the agent's OWN entity
|
|
191
|
-
// by its claimed self-uid instead. Falls back to the person-only pick for
|
|
192
|
-
// a person identity (or an agent whose self-entity didn't list), so the
|
|
193
|
-
// prs_* `--personal` path is unchanged.
|
|
180
|
+
// Self-entity listing. `/entity/by-type/:type` is type-filtered: querying
|
|
181
|
+
// `person` for an agent JWT returns no agent entity in production. Query
|
|
182
|
+
// the caller-scoped agent collection for a machine identity instead, then
|
|
183
|
+
// require the JWT-bound UID below. A machine caller must never fall back
|
|
184
|
+
// to a person entity, which could otherwise select somebody else's vault.
|
|
194
185
|
const isAgentIdentity = options.claims?.["custom:entityType"] === "agent";
|
|
195
186
|
const agentSelfUid = options.claims?.["custom:entityUid"] ?? "";
|
|
187
|
+
const selfEntities = await options.client.entity.listByType(
|
|
188
|
+
isAgentIdentity ? "agent" : "person",
|
|
189
|
+
);
|
|
196
190
|
const pick = isAgentIdentity
|
|
197
|
-
?
|
|
198
|
-
|
|
199
|
-
: pickCanonicalPersonEntity(persons);
|
|
191
|
+
? pickAgentSelfEntity(selfEntities, agentSelfUid)
|
|
192
|
+
: pickCanonicalPersonEntity(selfEntities);
|
|
200
193
|
if (pick?.bucketName) {
|
|
201
194
|
plan.push({
|
|
202
195
|
slug: "personal",
|
|
@@ -6459,6 +6459,77 @@ describe("US-004 — --personal for an AGENT machine identity (HARD GATE)", () =
|
|
|
6459
6459
|
"custom:entityUid": AGENT_UID,
|
|
6460
6460
|
};
|
|
6461
6461
|
|
|
6462
|
+
it("uses the newly minted machine ID token for --personal claims when no cache claims are available", async () => {
|
|
6463
|
+
const machineIdToken = `header.${Buffer.from(JSON.stringify(agentClaims)).toString("base64url")}.signature`;
|
|
6464
|
+
const listByType = vi.fn((type: string) =>
|
|
6465
|
+
Promise.resolve(type === "agent" ? [agentSelfEntity] : []),
|
|
6466
|
+
);
|
|
6467
|
+
const syncSpy = vi.fn().mockResolvedValue(defaultSyncResult());
|
|
6468
|
+
const deps = makeDeps({
|
|
6469
|
+
getAccessToken: vi.fn().mockResolvedValue(machineIdToken),
|
|
6470
|
+
// Simulate a newly minted token before any cache read is available.
|
|
6471
|
+
getIdTokenClaims: () => null,
|
|
6472
|
+
sync: syncSpy as unknown as RunnerDeps["sync"],
|
|
6473
|
+
createVaultClient: () => ({
|
|
6474
|
+
...makeVaultStub(),
|
|
6475
|
+
entity: { get: () => Promise.resolve(undefined as never), listByType },
|
|
6476
|
+
}),
|
|
6477
|
+
});
|
|
6478
|
+
|
|
6479
|
+
expect(await runRunner(["--personal"], deps)).toBe(0);
|
|
6480
|
+
expect(listByType).toHaveBeenCalledWith("agent");
|
|
6481
|
+
expect(syncSpy).toHaveBeenCalledTimes(1);
|
|
6482
|
+
expect(deps.stderr.events().some((event) => event.type === "auth-error")).toBe(false);
|
|
6483
|
+
});
|
|
6484
|
+
|
|
6485
|
+
it("uses the freshly authenticated machine claims over a stale human cache for --companies", async () => {
|
|
6486
|
+
const machineIdToken = `header.${Buffer.from(JSON.stringify(agentClaims)).toString("base64url")}.signature`;
|
|
6487
|
+
const listByType = vi.fn((type: string) =>
|
|
6488
|
+
Promise.resolve(type === "agent" ? [agentSelfEntity] : []),
|
|
6489
|
+
);
|
|
6490
|
+
const syncSpy = vi.fn().mockResolvedValue(defaultSyncResult());
|
|
6491
|
+
const deps = makeDeps({
|
|
6492
|
+
getAccessToken: vi.fn().mockResolvedValue(machineIdToken),
|
|
6493
|
+
// A pre-bootstrap human token must not make this machine run select a
|
|
6494
|
+
// person vault (or drop its own personal target altogether).
|
|
6495
|
+
getIdTokenClaims: () => ({ sub: "human-sub" }),
|
|
6496
|
+
sync: syncSpy as unknown as RunnerDeps["sync"],
|
|
6497
|
+
createVaultClient: () => ({
|
|
6498
|
+
...makeVaultStub({ memberships: [{ companyUid: "cmp_team" }] }),
|
|
6499
|
+
entity: {
|
|
6500
|
+
get: (uid: string) =>
|
|
6501
|
+
Promise.resolve(
|
|
6502
|
+
uid === "cmp_team"
|
|
6503
|
+
? ({
|
|
6504
|
+
uid,
|
|
6505
|
+
type: "company",
|
|
6506
|
+
slug: "team",
|
|
6507
|
+
bucketName: "hq-vault-cmp-team",
|
|
6508
|
+
status: "active",
|
|
6509
|
+
} as EntityInfo)
|
|
6510
|
+
: (undefined as never),
|
|
6511
|
+
),
|
|
6512
|
+
listByType,
|
|
6513
|
+
},
|
|
6514
|
+
}),
|
|
6515
|
+
});
|
|
6516
|
+
|
|
6517
|
+
expect(await runRunner(["--companies"], deps)).toBe(0);
|
|
6518
|
+
expect(listByType).toHaveBeenCalledWith("agent");
|
|
6519
|
+
expect(syncSpy).toHaveBeenCalledTimes(2);
|
|
6520
|
+
const plan = deps.stdout.events().find((event) => event.type === "fanout-plan") as Extract<
|
|
6521
|
+
RunnerEvent,
|
|
6522
|
+
{ type: "fanout-plan" }
|
|
6523
|
+
>;
|
|
6524
|
+
expect(plan.companies).toContainEqual(
|
|
6525
|
+
expect.objectContaining({
|
|
6526
|
+
uid: AGENT_UID,
|
|
6527
|
+
slug: "personal",
|
|
6528
|
+
personalMode: true,
|
|
6529
|
+
}),
|
|
6530
|
+
);
|
|
6531
|
+
});
|
|
6532
|
+
|
|
6462
6533
|
// -----------------------------------------------------------------------
|
|
6463
6534
|
// (1) RESOLUTION — UNBLOCKED by US-013.
|
|
6464
6535
|
//
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -1203,8 +1203,9 @@ export async function runRunner(
|
|
|
1203
1203
|
(() => getValidAccessToken(DEFAULT_COGNITO, { interactive: false }));
|
|
1204
1204
|
const clearSession =
|
|
1205
1205
|
deps.clearSession ?? invalidateCachedTokensByFingerprint;
|
|
1206
|
+
let initialAuthToken: string;
|
|
1206
1207
|
try {
|
|
1207
|
-
await getAccessToken();
|
|
1208
|
+
initialAuthToken = await getAccessToken();
|
|
1208
1209
|
} catch (err) {
|
|
1209
1210
|
if (
|
|
1210
1211
|
(err instanceof CognitoRefreshError && !err.requiresReauth) ||
|
|
@@ -1251,7 +1252,19 @@ export async function runRunner(
|
|
|
1251
1252
|
// Resolved here (not inside `parsed.companies`) so single-company runs also
|
|
1252
1253
|
// get author attribution. `null` is fine — share() simply omits the metadata.
|
|
1253
1254
|
const getClaims = deps.getIdTokenClaims ?? defaultGetIdTokenClaims;
|
|
1254
|
-
|
|
1255
|
+
// A machine token is minted immediately above and is an ID token. Make its
|
|
1256
|
+
// entity binding authoritative for this run: a stale human token cache can
|
|
1257
|
+
// otherwise make an authenticated agent look like a person during fanout
|
|
1258
|
+
// planning, which drops the agent's own personal-vault target. Human callers
|
|
1259
|
+
// still retain the cache as their primary claims source because their bearer
|
|
1260
|
+
// is normally an access token without the full ID-token profile.
|
|
1261
|
+
const authenticatedClaims = decodeJwtClaims(initialAuthToken);
|
|
1262
|
+
const authenticatedMachineClaims =
|
|
1263
|
+
authenticatedClaims?.["custom:entityType"] === "agent" &&
|
|
1264
|
+
typeof authenticatedClaims["custom:entityUid"] === "string"
|
|
1265
|
+
? authenticatedClaims
|
|
1266
|
+
: null;
|
|
1267
|
+
const claims = authenticatedMachineClaims ?? getClaims() ?? authenticatedClaims;
|
|
1255
1268
|
const uploadAuthor: UploadAuthor | undefined =
|
|
1256
1269
|
claims?.sub && claims?.email
|
|
1257
1270
|
? { userSub: claims.sub, email: claims.email }
|