@stigmer/server 3.39.1-dev.20260928194002 → 3.40.1
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/artifactstorage/artifact-storage.d.ts +10 -2
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +6 -3
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/boot/artifact-lane.d.ts +55 -0
- package/dist/boot/artifact-lane.d.ts.map +1 -0
- package/dist/boot/artifact-lane.js +74 -0
- package/dist/boot/artifact-lane.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +43 -19
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +11 -6
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +10 -5
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/agent/controller.d.ts +5 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts +18 -4
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +2 -2
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/create-steps.d.ts +29 -15
- package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/create-steps.js +118 -51
- package/dist/domain/agentexecution/create-steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +8 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +9 -4
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +3 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/artifact/file-server.d.ts +2 -1
- package/dist/domain/artifact/file-server.d.ts.map +1 -1
- package/dist/domain/artifact/file-server.js +6 -3
- package/dist/domain/artifact/file-server.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts +12 -3
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js +19 -9
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +8 -5
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +3 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts +7 -4
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +6 -2
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/memory/constants.d.ts +9 -0
- package/dist/domain/memory/constants.d.ts.map +1 -1
- package/dist/domain/memory/constants.js +9 -0
- package/dist/domain/memory/constants.js.map +1 -1
- package/dist/domain/memory/controller.d.ts +18 -6
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +18 -21
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +38 -10
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +87 -17
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +7 -4
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/session/controller.d.ts +7 -3
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +8 -2
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts +6 -2
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts +8 -4
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
- package/dist/domain/workflowexecution/update-status.js +3 -2
- package/dist/domain/workflowexecution/update-status.js.map +1 -1
- package/dist/extensions/identity.d.ts +28 -14
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +38 -0
- package/dist/extensions/identity.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +1 -13
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
- package/src/artifactstorage/artifact-storage.ts +12 -4
- package/src/boot/__tests__/artifact-lane.test.ts +71 -0
- package/src/boot/__tests__/compose.test.ts +73 -3
- package/src/boot/__tests__/config.test.ts +19 -0
- package/src/boot/artifact-lane.ts +96 -0
- package/src/boot/compose.ts +58 -21
- package/src/boot/config.ts +25 -15
- package/src/domain/agent/controller.ts +5 -2
- package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
- package/src/domain/agentexecution/controller.ts +32 -6
- package/src/domain/agentexecution/create-steps.ts +162 -62
- package/src/domain/agentinstance/controller.ts +8 -2
- package/src/domain/agentshare/controller.ts +12 -7
- package/src/domain/artifact/file-server.ts +8 -4
- package/src/domain/channelapp/controller.ts +38 -11
- package/src/domain/environment/controller.ts +11 -8
- package/src/domain/executioncontext/controller.ts +7 -4
- package/src/domain/mcpserver/controller.ts +6 -2
- package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
- package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
- package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
- package/src/domain/memory/constants.ts +11 -0
- package/src/domain/memory/controller.ts +36 -27
- package/src/domain/memory/steps.ts +112 -17
- package/src/domain/organization/controller.ts +7 -4
- package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
- package/src/domain/session/controller.ts +7 -3
- package/src/domain/skill/__tests__/skill.test.ts +4 -0
- package/src/domain/skill/controller.ts +8 -2
- package/src/domain/workflow/controller.ts +6 -2
- package/src/domain/workflowexecution/controller.ts +8 -4
- package/src/domain/workflowexecution/update-status.ts +3 -2
- package/src/extensions/__tests__/identity.test.ts +82 -0
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +39 -1
- package/src/extensions/identity.ts +47 -0
- package/src/pipeline/steps/guard-memory-capture.ts +4 -19
|
@@ -13,10 +13,15 @@
|
|
|
13
13
|
* cascade of same-org shares lives in the AGENT domain (ported with #6);
|
|
14
14
|
* share delete itself cascades nothing.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Every chain opens with Authorize. create asks can_edit on the referenced
|
|
17
|
+
* agent, then can_create_agent_share (AuthorizeResolvedTarget); create and
|
|
18
|
+
* delete run the shared tuple-lifecycle steps against the composed
|
|
19
|
+
* lifecycle; getByReference authorizes the loaded share as `get` would;
|
|
20
|
+
* getByAgent and list narrow through the composed list read scope. The
|
|
21
|
+
* public share-link read skips the Authorizer through is_public, and the
|
|
22
|
+
* member read asks can_view on the organization before the share loads
|
|
23
|
+
* (AuthorizeMemberAudience). Per-RPC posture:
|
|
24
|
+
* docs/authorization-coverage.md §9.
|
|
20
25
|
*
|
|
21
26
|
* Proven by agentshare.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
22
27
|
* __tests__/agentshare.test.ts and __tests__/store-faults.test.ts.
|
|
@@ -657,9 +662,9 @@ function newLoadSharesByAgentStep(
|
|
|
657
662
|
const LIST_RESULT_KEY = "listResult";
|
|
658
663
|
|
|
659
664
|
/**
|
|
660
|
-
* List — org + labels filter (AND semantics), newest first
|
|
661
|
-
*
|
|
662
|
-
* list.go records.
|
|
665
|
+
* List — org + labels filter (AND semantics), newest first, narrowed to
|
|
666
|
+
* the caller's authorized rows by the composed list read scope.
|
|
667
|
+
* Unpaginated, exactly as Go's list.go records.
|
|
663
668
|
*/
|
|
664
669
|
async function list(
|
|
665
670
|
deps: AgentShareControllerDeps,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The artifact HTTP file server — ports the local-download lane from Go
|
|
3
3
|
* pkg/server/server.go (artifactDownloadHandler + the boot block, lines
|
|
4
|
-
* 849–926): a second listener on 127.0.0.1:ARTIFACT_HTTP_PORT (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 849–926): a second listener on 127.0.0.1:ARTIFACT_HTTP_PORT (unset: the
|
|
5
|
+
* unified port + 1, ephemeral beside an ephemeral unified port — the rule is
|
|
6
|
+
* boot/artifact-lane.ts), started only when artifact storage is LOCAL,
|
|
7
|
+
* serving GET /<key> as the exact bytes LocalArtifactStorage wrote. Unlike
|
|
8
|
+
* Go, a bind failure fails the boot: the composition binds the lane before
|
|
9
|
+
* SERVING and lets listen()'s rejection stand (stigmer#1089). The lane
|
|
7
10
|
* dissolved into its owning domain per the ratified D4 decision — it is
|
|
8
11
|
* NOT a unified-port lane (Go runs it as a separate listener, and so does
|
|
9
12
|
* this port).
|
|
@@ -39,7 +42,8 @@ export interface ArtifactFileServerOptions {
|
|
|
39
42
|
|
|
40
43
|
export interface ArtifactFileServer {
|
|
41
44
|
/**
|
|
42
|
-
* Binds <host>:<port> (
|
|
45
|
+
* Binds <host>:<port> (port 0 picks ephemeral) and resolves the port it
|
|
46
|
+
* bound; a bind failure rejects, and the composition fails the boot on it.
|
|
43
47
|
* The host is the composition root's call: 127.0.0.1 everywhere by
|
|
44
48
|
* default (ARTIFACT_HTTP_HOST, DD-013) — download URLs are minted for
|
|
45
49
|
* the local machine — and 0.0.0.0 only inside the official container
|
|
@@ -14,9 +14,15 @@
|
|
|
14
14
|
* not_search_indexed — configuration reached through its parent surface,
|
|
15
15
|
* not a library artifact), so no index steps appear in any chain.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Every chain opens with Authorize; create and delete run the shared
|
|
18
|
+
* tuple-lifecycle steps against the composed lifecycle; getByReference
|
|
19
|
+
* loads, then authorizes the loaded app exactly as `get` would
|
|
20
|
+
* (AuthorizeResolvedTarget). listByOrg narrows the organization's apps
|
|
21
|
+
* through the composed list read scope to the ones a `get` would return:
|
|
22
|
+
* the model shows a channel app to its owners, explicit grants and the
|
|
23
|
+
* organization's admins, never to plain members or viewers
|
|
24
|
+
* (fga/model/agentic/channel_app.fga, stigmer/stigmer#1384). Per-RPC
|
|
25
|
+
* posture: docs/authorization-coverage.md §13.
|
|
20
26
|
*
|
|
21
27
|
* Proven by channelapp.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
22
28
|
* and __tests__/channelapp.test.ts.
|
|
@@ -42,6 +48,8 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
|
|
|
42
48
|
|
|
43
49
|
import type { Logger } from "../../boot/logger.js";
|
|
44
50
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
51
|
+
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
52
|
+
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
45
53
|
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
46
54
|
import type { SecretService } from "../../encryption/encryption.js";
|
|
47
55
|
import { apiResourceKindKey } from "../../pipeline/interceptors/apiresource.js";
|
|
@@ -106,6 +114,8 @@ export interface ChannelAppControllerDeps {
|
|
|
106
114
|
/** The composed tuple-lifecycle driver — undefined = the shared steps no-op (C2). */
|
|
107
115
|
readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
|
|
108
116
|
readonly secretService: SecretService;
|
|
117
|
+
/** The composed list read scope — listByOrg narrows through it; undefined = the OSS full scan (stigmer/stigmer#1384). */
|
|
118
|
+
readonly listReadScope: ListReadScope | undefined;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
/** Registers both channelapp services on the router (routes stage). */
|
|
@@ -389,8 +399,8 @@ async function getByReference(
|
|
|
389
399
|
const LIST_RESULT_KEY = "listResult";
|
|
390
400
|
|
|
391
401
|
/**
|
|
392
|
-
* ListByOrg —
|
|
393
|
-
* entry redacted. No pagination — the set is small by nature (typically
|
|
402
|
+
* ListByOrg — the ChannelApps of an organization the caller may view,
|
|
403
|
+
* newest-first, every entry redacted. No pagination — the set is small by nature (typically
|
|
394
404
|
* one app per provider per org), the OAuthApp listByOrg posture.
|
|
395
405
|
*/
|
|
396
406
|
async function listByOrg(
|
|
@@ -415,7 +425,7 @@ async function listByOrg(
|
|
|
415
425
|
),
|
|
416
426
|
)
|
|
417
427
|
.addStep(newValidateProtoStep())
|
|
418
|
-
.addStep(newListByOrgStep(deps.store, deps.logger))
|
|
428
|
+
.addStep(newListByOrgStep(deps.store, deps.listReadScope, deps.logger))
|
|
419
429
|
.build()
|
|
420
430
|
.execute(reqCtx);
|
|
421
431
|
|
|
@@ -431,12 +441,19 @@ async function listByOrg(
|
|
|
431
441
|
|
|
432
442
|
/**
|
|
433
443
|
* ListByOrg — Go list_by_org.go's domain step: full scan, malformed rows
|
|
434
|
-
* skipped with a warning, org equality filter,
|
|
435
|
-
* by spec-audit created_at descending
|
|
436
|
-
* entries before untimestamped ones).
|
|
444
|
+
* skipped with a warning, org equality filter, narrowed to the caller's
|
|
445
|
+
* apps, per-item redaction, sorted by spec-audit created_at descending
|
|
446
|
+
* (seconds then nanos; timestamped entries before untimestamped ones).
|
|
447
|
+
* The org predicate stays the step's own because with no scope composed
|
|
448
|
+
* the helper returns its input unchanged and never reads the org (the
|
|
449
|
+
* trusted-local full scan); with one, the scope is the last per-row
|
|
450
|
+
* predicate, so a composed driver is asked about the org's apps, never
|
|
451
|
+
* every tenant's, and only the kept apps are redacted (the OAuthApp
|
|
452
|
+
* list's order).
|
|
437
453
|
*/
|
|
438
454
|
function newListByOrgStep(
|
|
439
455
|
store: Store,
|
|
456
|
+
listReadScope: ListReadScope | undefined,
|
|
440
457
|
logger: Logger,
|
|
441
458
|
): PipelineStep<typeof ChannelAppQueryController.method.listByOrg.input> {
|
|
442
459
|
return {
|
|
@@ -455,7 +472,7 @@ function newListByOrgStep(
|
|
|
455
472
|
throw internalError(error, "failed to list ChannelApps");
|
|
456
473
|
}
|
|
457
474
|
|
|
458
|
-
const
|
|
475
|
+
const requested: ChannelApp[] = [];
|
|
459
476
|
for (const bytes of rows) {
|
|
460
477
|
let app: ChannelApp;
|
|
461
478
|
try {
|
|
@@ -467,8 +484,18 @@ function newListByOrgStep(
|
|
|
467
484
|
if ((app.metadata?.org ?? "") !== org) {
|
|
468
485
|
continue;
|
|
469
486
|
}
|
|
487
|
+
requested.push(app);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const apps = await restrictListByReadScope(
|
|
491
|
+
listReadScope,
|
|
492
|
+
ctx.callerIdentity,
|
|
493
|
+
ApiResourceKind.channel_app,
|
|
494
|
+
requested,
|
|
495
|
+
"",
|
|
496
|
+
);
|
|
497
|
+
for (const app of apps) {
|
|
470
498
|
redactChannelApp(app);
|
|
471
|
-
apps.push(app);
|
|
472
499
|
}
|
|
473
500
|
|
|
474
501
|
apps.sort((a, b) =>
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
* Proven by environment.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
12
12
|
* __tests__/environment.test.ts and __tests__/store-faults.test.ts.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* Every chain opens with Authorize; create, delete and updateVisibility run
|
|
15
|
+
* the shared tuple-lifecycle steps against the composed lifecycle;
|
|
16
|
+
* getByReference loads, then authorizes the loaded environment exactly as
|
|
17
|
+
* `get` would (AuthorizeResolvedTarget); list narrows through the composed
|
|
18
|
+
* list read scope. The visibility ceiling (org — secrets never resolve
|
|
19
|
+
* across the org boundary) and share restrictions come from the same proto
|
|
20
|
+
* config in every edition, keeping the error contract identical. Per-RPC
|
|
21
|
+
* posture: docs/authorization-coverage.md §3.
|
|
19
22
|
*/
|
|
20
23
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
21
24
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
@@ -734,9 +737,9 @@ async function getSecretValue(
|
|
|
734
737
|
const LIST_RESULT_KEY = "listResult";
|
|
735
738
|
|
|
736
739
|
/**
|
|
737
|
-
* List — org + labels filter with per-item redaction
|
|
738
|
-
*
|
|
739
|
-
* exactly as Go's list.go records.
|
|
740
|
+
* List — org + labels filter with per-item redaction, narrowed to the
|
|
741
|
+
* caller's authorized rows by the composed list read scope. Unpaginated
|
|
742
|
+
* (returns all matches), exactly as Go's list.go records.
|
|
740
743
|
*/
|
|
741
744
|
async function list(
|
|
742
745
|
deps: EnvironmentControllerDeps,
|
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
* (the decrypt-lane matrix conformance deliberately never exercises —
|
|
26
26
|
* its harness authenticates as a user).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Every chain opens with Authorize; create asks can_create_execution_in on
|
|
29
|
+
* the organization (AuthorizeCreate), and create and delete run the shared
|
|
30
|
+
* tuple-lifecycle steps against the composed lifecycle; getByReference
|
|
31
|
+
* loads, then authorizes the loaded context exactly as `get` would
|
|
32
|
+
* (AuthorizeResolvedTarget). The redaction and decrypt-lane contracts are
|
|
33
|
+
* the same in every edition, keeping the error contract identical. Per-RPC
|
|
34
|
+
* posture: docs/authorization-coverage.md §5.
|
|
32
35
|
*/
|
|
33
36
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
34
37
|
|
|
@@ -19,8 +19,12 @@
|
|
|
19
19
|
* managed-env wiring is a disclosed, deliberately unpinned composition
|
|
20
20
|
* artifact).
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Every chain opens with Authorize; create, delete and updateVisibility run
|
|
23
|
+
* the shared tuple-lifecycle steps against the composed lifecycle;
|
|
24
|
+
* getByReference loads, then authorizes the loaded server exactly as `get`
|
|
25
|
+
* would (AuthorizeResolvedTarget). The connect/OAuth handlers evaluate
|
|
26
|
+
* their annotation through authorizeDirect, after the load where they load
|
|
27
|
+
* (#224). Per-RPC posture: docs/authorization-coverage.md §20.
|
|
24
28
|
*
|
|
25
29
|
* Proven by mcpserver.conformance.test.ts +
|
|
26
30
|
* agent-mcpserver-references.conformance.test.ts +
|
|
@@ -42,7 +42,7 @@ function memoryCtx(spec: {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
describe("ResolveMemoryDefaults with an admitted capture credential", () => {
|
|
45
|
-
it("writes the proved subject and overrides provenance.session_id", () => {
|
|
45
|
+
it("writes the proved subject and overrides provenance.session_id", async () => {
|
|
46
46
|
const ctx = memoryCtx({
|
|
47
47
|
provenance: {
|
|
48
48
|
sessionId: "ses_runner_reported",
|
|
@@ -54,7 +54,7 @@ describe("ResolveMemoryDefaults with an admitted capture credential", () => {
|
|
|
54
54
|
subjectIdentityAccountId: "ida_human",
|
|
55
55
|
provedSessionId: "ses_proved",
|
|
56
56
|
});
|
|
57
|
-
newResolveMemoryDefaultsStep().execute(ctx);
|
|
57
|
+
await newResolveMemoryDefaultsStep().execute(ctx);
|
|
58
58
|
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe("ida_human");
|
|
59
59
|
expect(ctx.newState.spec?.provenance?.sessionId).toBe("ses_proved");
|
|
60
60
|
// The threaded agent id survives; the forged tool_call_id never does.
|
|
@@ -62,21 +62,21 @@ describe("ResolveMemoryDefaults with an admitted capture credential", () => {
|
|
|
62
62
|
expect(ctx.newState.spec?.provenance?.toolCallId).toBe("");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("creates provenance when the request supplied none", () => {
|
|
65
|
+
it("creates provenance when the request supplied none", async () => {
|
|
66
66
|
const ctx = memoryCtx({});
|
|
67
67
|
ctx.set(MEMORY_CAPTURE_CREDENTIAL_KEY, {
|
|
68
68
|
subjectIdentityAccountId: "ida_human",
|
|
69
69
|
provedSessionId: "ses_proved",
|
|
70
70
|
});
|
|
71
|
-
newResolveMemoryDefaultsStep().execute(ctx);
|
|
71
|
+
await newResolveMemoryDefaultsStep().execute(ctx);
|
|
72
72
|
expect(ctx.newState.spec?.provenance?.sessionId).toBe("ses_proved");
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it("without the handoff the OSS arms stand", () => {
|
|
75
|
+
it("without the handoff the OSS arms stand", async () => {
|
|
76
76
|
const ctx = memoryCtx({
|
|
77
77
|
provenance: { sessionId: "ses_supplied", toolCallId: "forged" },
|
|
78
78
|
});
|
|
79
|
-
newResolveMemoryDefaultsStep().execute(ctx);
|
|
79
|
+
await newResolveMemoryDefaultsStep().execute(ctx);
|
|
80
80
|
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe("");
|
|
81
81
|
expect(ctx.newState.spec?.provenance?.sessionId).toBe("ses_supplied");
|
|
82
82
|
expect(ctx.newState.spec?.provenance?.toolCallId).toBe("");
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins memory's double opt-in and its subject at the composition root
|
|
3
|
+
* (stigmer#1387), over a real boot under the BUILT-IN posture (a unit that
|
|
4
|
+
* declares require-authentication and registers no Authorizer — the
|
|
5
|
+
* open-source self-host with sign-in), with two signed-in people on the
|
|
6
|
+
* wire:
|
|
7
|
+
*
|
|
8
|
+
* - Carol founds an organization with memory on. With her own switch
|
|
9
|
+
* still off (the default), her capture is refused with the account
|
|
10
|
+
* copy — before this fix it was stored, filed under nobody.
|
|
11
|
+
* - Once she turns her own switch on, her capture is filed under HER
|
|
12
|
+
* account id, she can read and confirm it (the model's subject tuple
|
|
13
|
+
* names her), and Dave, a member of the same organization, can
|
|
14
|
+
* neither read it nor see it listed: memory is subject-only.
|
|
15
|
+
* - Dave, whose own switch is off, is refused exactly as Carol was.
|
|
16
|
+
*
|
|
17
|
+
* Recall is not driven here: an execution create needs an engine, and the
|
|
18
|
+
* recall step runs after the engine check. Its gates are pinned
|
|
19
|
+
* step-for-step in domain/agentexecution/__tests__/create-steps.test.ts,
|
|
20
|
+
* over the same identity-account reads this boot composes.
|
|
21
|
+
*/
|
|
22
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
import { clone, create } from "@bufbuild/protobuf";
|
|
27
|
+
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
28
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
29
|
+
|
|
30
|
+
import { MemorySchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
31
|
+
import { MemoryCommandController } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/command_pb";
|
|
32
|
+
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
33
|
+
import { MemoryQueryController } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/query_pb";
|
|
34
|
+
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
35
|
+
import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
36
|
+
import {
|
|
37
|
+
IdentityAccountPreferencesSchema,
|
|
38
|
+
IdentityAccountSpecSchema,
|
|
39
|
+
} from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/spec_pb";
|
|
40
|
+
import { IdentityAccountCommandController } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/command_pb";
|
|
41
|
+
import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
|
|
42
|
+
|
|
43
|
+
import { loadConfig } from "../../../boot/config.js";
|
|
44
|
+
import { composeServer } from "../../../boot/compose.js";
|
|
45
|
+
import type { ComposedServer } from "../../../boot/compose.js";
|
|
46
|
+
import type { ServerExtension } from "../../../extensions/registry.js";
|
|
47
|
+
import {
|
|
48
|
+
baseConfig,
|
|
49
|
+
fakeJwt,
|
|
50
|
+
fakeVerifier,
|
|
51
|
+
silentLogger,
|
|
52
|
+
transportFor,
|
|
53
|
+
} from "../../../extensions/__tests__/composed-support.js";
|
|
54
|
+
import { MEMORY_ACCOUNT_DISABLED_MESSAGE } from "../constants.js";
|
|
55
|
+
|
|
56
|
+
const CAROL = "fake|carol";
|
|
57
|
+
const DAVE = "fake|dave";
|
|
58
|
+
const ORG = "memory-person-org";
|
|
59
|
+
|
|
60
|
+
async function failureOf(promise: Promise<unknown>): Promise<ConnectError> {
|
|
61
|
+
try {
|
|
62
|
+
await promise;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
expect(error).toBeInstanceOf(ConnectError);
|
|
65
|
+
return error as ConnectError;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("expected the call to fail");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function memoryInput(content: string) {
|
|
71
|
+
return create(MemorySchema, {
|
|
72
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
73
|
+
kind: "Memory",
|
|
74
|
+
metadata: { org: ORG },
|
|
75
|
+
spec: { content },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("memory where callers are persons (built-in posture, composed server)", () => {
|
|
80
|
+
let dir: string;
|
|
81
|
+
let server: ComposedServer;
|
|
82
|
+
let port: number;
|
|
83
|
+
let carolAccount: IdentityAccount;
|
|
84
|
+
|
|
85
|
+
const asCarol = () => transportFor(port, fakeJwt(CAROL, "carol@example.com"));
|
|
86
|
+
const asDave = () => transportFor(port, fakeJwt(DAVE, "dave@example.com"));
|
|
87
|
+
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
dir = mkdtempSync(path.join(tmpdir(), "memory-person-consent-composed-"));
|
|
90
|
+
const unit: ServerExtension = {
|
|
91
|
+
name: "fake-oidc-only",
|
|
92
|
+
requireAuthentication: true,
|
|
93
|
+
identityVerifiers: [fakeVerifier],
|
|
94
|
+
};
|
|
95
|
+
server = await composeServer({
|
|
96
|
+
config: loadConfig(baseConfig(dir)),
|
|
97
|
+
logger: silentLogger,
|
|
98
|
+
extensions: [unit],
|
|
99
|
+
portOverride: 0,
|
|
100
|
+
host: "127.0.0.1",
|
|
101
|
+
});
|
|
102
|
+
port = await server.start();
|
|
103
|
+
|
|
104
|
+
// Carol provisions and founds the organization with memory on; Dave
|
|
105
|
+
// provisions afterwards and is a member of it (the membership rules
|
|
106
|
+
// run at a person's first provisioning).
|
|
107
|
+
carolAccount = await createClient(
|
|
108
|
+
IdentityAccountCommandController,
|
|
109
|
+
asCarol(),
|
|
110
|
+
).provisionMyAccount({});
|
|
111
|
+
await createClient(OrganizationCommandController, asCarol()).create({
|
|
112
|
+
apiVersion: "tenancy.stigmer.ai/v1",
|
|
113
|
+
kind: "Organization",
|
|
114
|
+
metadata: { name: ORG, slug: ORG, org: "" },
|
|
115
|
+
spec: { description: ORG, preferences: { memoryEnabled: true } },
|
|
116
|
+
});
|
|
117
|
+
await createClient(
|
|
118
|
+
IdentityAccountCommandController,
|
|
119
|
+
asDave(),
|
|
120
|
+
).provisionMyAccount({});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterAll(async () => {
|
|
124
|
+
await server.shutdown();
|
|
125
|
+
rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("refuses a person whose own switch is off, with the account's copy", async () => {
|
|
129
|
+
const failure = await failureOf(
|
|
130
|
+
createClient(MemoryCommandController, asCarol()).create(
|
|
131
|
+
memoryInput("Carol prefers OpenTofu."),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
expect(failure.code).toBe(Code.FailedPrecondition);
|
|
135
|
+
expect(failure.rawMessage).toBe(MEMORY_ACCOUNT_DISABLED_MESSAGE);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("files the fact under the person once they opt in, and it stays theirs alone", async () => {
|
|
139
|
+
const carolId = carolAccount.metadata?.id ?? "";
|
|
140
|
+
expect(carolId).not.toBe("");
|
|
141
|
+
const optedIn = clone(IdentityAccountSchema, carolAccount);
|
|
142
|
+
optedIn.spec ??= create(IdentityAccountSpecSchema);
|
|
143
|
+
optedIn.spec.preferences ??= create(IdentityAccountPreferencesSchema);
|
|
144
|
+
optedIn.spec.preferences.memoryEnabled = true;
|
|
145
|
+
await createClient(IdentityAccountCommandController, asCarol()).update(
|
|
146
|
+
optedIn,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const carolMemories = createClient(MemoryCommandController, asCarol());
|
|
150
|
+
const created = await carolMemories.create(
|
|
151
|
+
memoryInput("Carol prefers OpenTofu."),
|
|
152
|
+
);
|
|
153
|
+
const memoryId = created.metadata?.id ?? "";
|
|
154
|
+
expect(created.spec?.subjectIdentityAccountId).toBe(carolId);
|
|
155
|
+
|
|
156
|
+
// The subject tuple names Carol: she decides on her own memory.
|
|
157
|
+
const confirmed = await carolMemories.confirm({ value: memoryId });
|
|
158
|
+
expect(confirmed.status?.lifecycleState).toBe(
|
|
159
|
+
MemoryLifecycleState.lifecycle_state_confirmed,
|
|
160
|
+
);
|
|
161
|
+
const carolReads = createClient(MemoryQueryController, asCarol());
|
|
162
|
+
expect((await carolReads.get({ value: memoryId })).metadata?.id).toBe(
|
|
163
|
+
memoryId,
|
|
164
|
+
);
|
|
165
|
+
expect(
|
|
166
|
+
(await carolReads.list({ org: ORG })).items.map((m) => m.metadata?.id),
|
|
167
|
+
).toContain(memoryId);
|
|
168
|
+
|
|
169
|
+
// Dave shares the organization, not the memory.
|
|
170
|
+
const daveReads = createClient(MemoryQueryController, asDave());
|
|
171
|
+
const failure = await failureOf(daveReads.get({ value: memoryId }));
|
|
172
|
+
expect(failure.code).toBe(Code.PermissionDenied);
|
|
173
|
+
expect(
|
|
174
|
+
(await daveReads.list({ org: ORG })).items.map((m) => m.metadata?.id),
|
|
175
|
+
).not.toContain(memoryId);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("refuses the second person exactly as the first, while their switch is off", async () => {
|
|
179
|
+
const failure = await failureOf(
|
|
180
|
+
createClient(MemoryCommandController, asDave()).create(
|
|
181
|
+
memoryInput("Dave deploys on Fridays."),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
expect(failure.code).toBe(Code.FailedPrecondition);
|
|
185
|
+
expect(failure.rawMessage).toBe(MEMORY_ACCOUNT_DISABLED_MESSAGE);
|
|
186
|
+
});
|
|
187
|
+
});
|