Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
],
"scripts": {
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"build": "pnpm exec vite build && pnpm exec tsc --build --force",
"build": "pnpm exec vite build && rm -f tsconfig.tsbuildinfo && pnpm exec tsc --build && test -f dist/index.d.ts && test -f dist/internal/cloudflare-coordinator.d.ts",
"typecheck": "pnpm exec tsc --noEmit",
"generate:test-schema": "pnpm exec tsx scripts/generate-test-schema.ts"
},
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ export {
startMaintenanceJob,
updateMaintenanceJob,
} from "./maintenance-jobs.js";
export * from "./memory-filter-schema.js";
export * from "./memory-kinds.js";
export type {
DerivedMemoryRole,
Expand Down Expand Up @@ -683,6 +684,19 @@ export {
mapPiEventPayload,
PI_FLUSH_ONLY_EVENTS,
} from "./pi-hooks.js";
export type {
PiObserverResolveErr,
PiObserverResolveInput,
PiObserverResolveOk,
PiObserverResolveReason,
PiObserverResolveResult,
} from "./pi-observer-config.js";
export {
describePiObserverStatus,
hasExplicitObserverEnvOverride,
resolvePiAgentDir,
resolvePiObserverConfig,
} from "./pi-observer-config.js";
export type {
BlockedPolicyTeamDeviceEligibilityResult,
DerivePolicyTeamDeviceEligibilityInput,
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/memory-filter-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Tool-exposed memory filter contract.
*
* Single source of truth for the filter keys and value types accepted by both
* memory tool surfaces: the MCP server tool schemas (pinned to this catalog by
* an exact parity test in @codemem/mcp-server) and the viewer-server HTTP
* routes (validated directly against this catalog). Keeping one catalog means
* a filter added here cannot be silently omitted from either surface, so
* exclusion filters can never fail open and return broader results than the
* client requested.
*
* Insertion order mirrors the MCP tool schema key order.
*/

export type MemoryFilterFieldType =
| "string"
| "string-array"
| "int"
| "number"
| "boolean-or-string";

export const MEMORY_FILTER_FIELD_TYPES = {
kind: "string",
project: "string",
scope_id: "string",
include_scope_ids: "string-array",
exclude_scope_ids: "string-array",
visibility: "string-array",
include_visibility: "string-array",
exclude_visibility: "string-array",
include_workspace_ids: "string-array",
exclude_workspace_ids: "string-array",
include_workspace_kinds: "string-array",
exclude_workspace_kinds: "string-array",
include_actor_ids: "string-array",
exclude_actor_ids: "string-array",
include_trust_states: "string-array",
exclude_trust_states: "string-array",
ownership_scope: "string",
personal_first: "boolean-or-string",
trust_bias: "string",
widen_shared_when_weak: "boolean-or-string",
widen_shared_min_personal_results: "int",
widen_shared_min_personal_score: "number",
} as const satisfies Record<string, MemoryFilterFieldType>;

export type MemoryFilterName = keyof typeof MEMORY_FILTER_FIELD_TYPES;

/** Sorted filter names exposed by memory_schema on both surfaces. */
export const MEMORY_FILTER_NAMES = Object.keys(
MEMORY_FILTER_FIELD_TYPES,
).toSorted() as MemoryFilterName[];

/** Check a raw request value against a filter field's declared type. */
export function memoryFilterValueMatchesType(
value: unknown,
fieldType: MemoryFilterFieldType,
): boolean {
switch (fieldType) {
case "string":
return typeof value === "string";
case "string-array":
return Array.isArray(value) && value.every((item) => typeof item === "string");
case "int":
return typeof value === "number" && Number.isInteger(value);
case "number":
return typeof value === "number" && Number.isFinite(value);
case "boolean-or-string":
return typeof value === "boolean" || typeof value === "string";
}
}
16 changes: 16 additions & 0 deletions packages/core/src/observer-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ describe("ObserverAuthAdapter", () => {
expect(result.source).toBe("oauth");
});

it("falls back to pi token after env/oauth", () => {
const adapter = new ObserverAuthAdapter();
const result = adapter.resolve({ piToken: "tok-pi" });
expect(result.token).toBe("tok-pi");
expect(result.source).toBe("pi");
});

it("explicit and env beat pi token", () => {
const adapter = new ObserverAuthAdapter();
expect(adapter.resolve({ explicitToken: "tok-explicit", piToken: "tok-pi" }).source).toBe(
"explicit",
);
expect(adapter.resolve({ envTokens: ["tok-env"], piToken: "tok-pi" }).source).toBe("env");
expect(adapter.resolve({ oauthToken: "tok-oauth", piToken: "tok-pi" }).source).toBe("oauth");
});

it("returns no token with source=none", () => {
const adapter = new ObserverAuthAdapter({ source: "none" });
const result = adapter.resolve({ explicitToken: "ignored" });
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/observer-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,12 +275,17 @@ export interface ObserverAuthResolveOptions {
explicitToken?: string | null;
envTokens?: string[];
oauthToken?: string | null;
/**
* In-memory credential from pi auth.json (D8). Used only when no explicit,
* env, or oauth token is available. Never persist or log this value.
*/
piToken?: string | null;
forceRefresh?: boolean;
}

/**
* Resolves auth credentials through a configurable cascade:
* explicit → env → oauth → file → command.
* explicit → env → oauth → pi → file → command.
*
* Results from file/command sources are cached for `cacheTtlS` seconds.
*/
Expand Down Expand Up @@ -314,6 +319,7 @@ export class ObserverAuthAdapter {
const explicitToken = opts?.explicitToken ?? null;
const envTokens = opts?.envTokens ?? [];
const oauthToken = opts?.oauthToken ?? null;
const piToken = opts?.piToken ?? null;
const forceRefresh = opts?.forceRefresh ?? false;

if (source === "none") return noAuth();
Expand Down Expand Up @@ -342,6 +348,11 @@ export class ObserverAuthAdapter {
token = oauthToken;
tokenSource = "oauth";
}
// D8: pi auth.json credential at point of use (after explicit/env/oauth).
if (!token && piToken) {
token = piToken;
tokenSource = "pi";
}
} else if (source === "env") {
token = envTokens.find((t) => !!t) ?? null;
if (token) tokenSource = "env";
Expand Down
Loading