Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ All notable changes to LifeOS are documented in this file.

- Plugin installation lookup, conflict replay, and revocation now carry authenticated workspace and installing-user authority through the PostgreSQL boundary; the durable record contains no plaintext plugin secret, token, credential, or password material.
- Calendar local disconnect never accepts client-selected ownership as authority, never reads provider secret handles, revalidates durable revocation evidence against the signed workspace+user context, and maps absent or differently owned connections to the same public not-found result.
- Goal, project, and task create/list routes now reject bare client-selected `x-workspace-id` authority and require the same short-lived signed `life-os.workspace.v1` context used by planning search and durable Today.
- The data-rights request ledger keeps personal export payloads out of durable audit rows and normalizes primary-key/idempotency collisions before dependency errors can escape the service boundary.
- The commercial-development model account no longer performs Docker commands, never receives Docker-socket authority, and cannot trigger provider-wide model discovery through the credential bridge.
- The scheduled live-model harness uses only `NVIDIA_NIM_API_KEY`, seeds it through the encrypted contextual-orchestrator credential registry, installs hash-locked dependencies from an exact commit, confines LifeOS traffic to loopback, allowlists NVIDIA NIM egress, and excludes provider credentials, prompts, responses, traces, and hidden reasoning from retained artifacts.
Expand Down
101 changes: 58 additions & 43 deletions apps/planning-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'reflect-metadata';
import {
BadRequestException,
Body,
Controller,
Get,
Expand Down Expand Up @@ -53,15 +52,6 @@ interface PassthroughResponse {
setHeader(name: string, value: string): void;
}

/** Requires the tenant workspace boundary used by legacy planning operations. */
function requireWorkspaceId(value: string | undefined): string {
const workspaceId = value?.trim();
if (!workspaceId) {
throw new BadRequestException('x-workspace-id header is required');
}
return workspaceId;
}

/** Returns a stable not-found problem without disclosing another tenant's state. */
function todayNotFound(): HttpException {
return new HttpException(
Expand Down Expand Up @@ -197,103 +187,128 @@ export class PlanningController {
}
}

/** Creates a goal inside the caller's required workspace. */
/** Creates a goal inside the signed gateway workspace. */
@Post('goals')
async createGoal(
@Headers('x-workspace-id') workspaceHeader: string | undefined,
@Headers('x-life-os-workspace-id') workspaceId: string | undefined,
@Headers('x-life-os-context-issued-at') issuedAt: string | undefined,
@Headers('x-life-os-context-signature') signature: string | undefined,
@Body() body: { title?: unknown },
): Promise<Goal> {
try {
return await this.planningService.createGoal(
requireWorkspaceId(workspaceHeader),
{
title: requireTitle(body),
},
const trustedWorkspaceId = requireTrustedWorkspaceContext(
{ workspaceId, issuedAt, signature },
process.env.PLANNING_GATEWAY_CONTEXT_SECRET,
);
return await this.planningService.createGoal(trustedWorkspaceId, {
title: requireTitle(body),
});
} catch (error) {
throw toHttpException(error);
}
}

/** Lists goals belonging to the caller's required workspace. */
/** Lists goals belonging to the signed gateway workspace. */
@Get('goals')
async listGoals(
@Headers('x-workspace-id') workspaceHeader: string | undefined,
@Headers('x-life-os-workspace-id') workspaceId: string | undefined,
@Headers('x-life-os-context-issued-at') issuedAt: string | undefined,
@Headers('x-life-os-context-signature') signature: string | undefined,
): Promise<Goal[]> {
try {
return await this.planningService.listGoals(
requireWorkspaceId(workspaceHeader),
const trustedWorkspaceId = requireTrustedWorkspaceContext(
{ workspaceId, issuedAt, signature },
process.env.PLANNING_GATEWAY_CONTEXT_SECRET,
);
return await this.planningService.listGoals(trustedWorkspaceId);
} catch (error) {
throw toHttpException(error);
}
}

/** Creates a project below a workspace-owned goal. */
/** Creates a project below a goal in the signed gateway workspace. */
@Post('goals/:goalId/projects')
async createProject(
@Headers('x-workspace-id') workspaceHeader: string | undefined,
@Headers('x-life-os-workspace-id') workspaceId: string | undefined,
@Headers('x-life-os-context-issued-at') issuedAt: string | undefined,
@Headers('x-life-os-context-signature') signature: string | undefined,
@Param('goalId') goalId: string,
@Body() body: { title?: unknown },
): Promise<Project> {
try {
return await this.planningService.createProject(
requireWorkspaceId(workspaceHeader),
{
goalId,
title: requireTitle(body),
},
const trustedWorkspaceId = requireTrustedWorkspaceContext(
{ workspaceId, issuedAt, signature },
process.env.PLANNING_GATEWAY_CONTEXT_SECRET,
);
return await this.planningService.createProject(trustedWorkspaceId, {
goalId,
title: requireTitle(body),
});
} catch (error) {
throw toHttpException(error);
}
}

/** Lists projects below a workspace-owned goal. */
/** Lists projects below a goal in the signed gateway workspace. */
@Get('goals/:goalId/projects')
async listProjects(
@Headers('x-workspace-id') workspaceHeader: string | undefined,
@Headers('x-life-os-workspace-id') workspaceId: string | undefined,
@Headers('x-life-os-context-issued-at') issuedAt: string | undefined,
@Headers('x-life-os-context-signature') signature: string | undefined,
@Param('goalId') goalId: string,
): Promise<Project[]> {
try {
const trustedWorkspaceId = requireTrustedWorkspaceContext(
{ workspaceId, issuedAt, signature },
process.env.PLANNING_GATEWAY_CONTEXT_SECRET,
);
return await this.planningService.listProjects(
requireWorkspaceId(workspaceHeader),
trustedWorkspaceId,
goalId,
);
} catch (error) {
throw toHttpException(error);
}
}

/** Creates a task below a workspace-owned project. */
/** Creates a task below a project in the signed gateway workspace. */
@Post('projects/:projectId/tasks')
async createTask(
@Headers('x-workspace-id') workspaceHeader: string | undefined,
@Headers('x-life-os-workspace-id') workspaceId: string | undefined,
@Headers('x-life-os-context-issued-at') issuedAt: string | undefined,
@Headers('x-life-os-context-signature') signature: string | undefined,
@Param('projectId') projectId: string,
@Body() body: { title?: unknown },
): Promise<Task> {
try {
return await this.planningService.createTask(
requireWorkspaceId(workspaceHeader),
{
projectId,
title: requireTitle(body),
},
const trustedWorkspaceId = requireTrustedWorkspaceContext(
{ workspaceId, issuedAt, signature },
process.env.PLANNING_GATEWAY_CONTEXT_SECRET,
);
return await this.planningService.createTask(trustedWorkspaceId, {
projectId,
title: requireTitle(body),
});
} catch (error) {
throw toHttpException(error);
}
}

/** Lists tasks below a workspace-owned project. */
/** Lists tasks below a project in the signed gateway workspace. */
@Get('projects/:projectId/tasks')
async listTasks(
@Headers('x-workspace-id') workspaceHeader: string | undefined,
@Headers('x-life-os-workspace-id') workspaceId: string | undefined,
@Headers('x-life-os-context-issued-at') issuedAt: string | undefined,
@Headers('x-life-os-context-signature') signature: string | undefined,
@Param('projectId') projectId: string,
): Promise<Task[]> {
try {
const trustedWorkspaceId = requireTrustedWorkspaceContext(
{ workspaceId, issuedAt, signature },
process.env.PLANNING_GATEWAY_CONTEXT_SECRET,
);
return await this.planningService.listTasks(
requireWorkspaceId(workspaceHeader),
trustedWorkspaceId,
projectId,
);
} catch (error) {
Expand Down
Loading
Loading