@amoree-code/ocean 0.0.0-stage → 1.2.0
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/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- package/templates/workspace/sessions/.gitkeep +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { KNOWLEDGE_DIR, PERSONAL_DIR } from "@ocean/core/shared/paths.js";
|
|
2
|
+
import { searchRecords, writeRecord } from "./memory-knowledge-operations.js";
|
|
3
|
+
import { gateOperation, operationResult, } from "./operation-contract.js";
|
|
4
|
+
import { projectCreate, projectDetect, projectUpdate, } from "./project-operations.js";
|
|
5
|
+
import { taskCompleteOperation, taskCreate, taskGet, taskList, taskUpdate, } from "./task-operations.js";
|
|
6
|
+
// Table-driven dispatch over the focused operation modules (task/memory/knowledge/project).
|
|
7
|
+
// This module is the sole public entry point — see operation-scope.ts, record-io.ts,
|
|
8
|
+
// task-operations.ts, memory-knowledge-operations.ts, and project-operations.ts for the
|
|
9
|
+
// implementations.
|
|
10
|
+
export async function runOperation(operation, classification, budget, options = {}) {
|
|
11
|
+
const gate = gateOperation(operation, classification, budget, options.approval);
|
|
12
|
+
if (!gate.ok)
|
|
13
|
+
return operationResult(operation, gate.reason);
|
|
14
|
+
const bounded = budget;
|
|
15
|
+
const cwd = options.cwd ?? process.cwd();
|
|
16
|
+
switch (operation) {
|
|
17
|
+
case "task.get":
|
|
18
|
+
return taskGet(classification, bounded, options, cwd);
|
|
19
|
+
case "task.list":
|
|
20
|
+
return taskList(classification, bounded, options, cwd);
|
|
21
|
+
case "task.update":
|
|
22
|
+
return taskUpdate(classification, bounded, options, cwd);
|
|
23
|
+
case "task.complete":
|
|
24
|
+
return taskCompleteOperation(classification, bounded, options, cwd);
|
|
25
|
+
case "task.create":
|
|
26
|
+
return taskCreate(classification, options, cwd);
|
|
27
|
+
case "memory.search":
|
|
28
|
+
return searchRecords("memory.search", "memory", [PERSONAL_DIR], classification, bounded, options);
|
|
29
|
+
case "knowledge.search":
|
|
30
|
+
return searchRecords("knowledge.search", "knowledge", [KNOWLEDGE_DIR], classification, bounded, options);
|
|
31
|
+
case "memory.write":
|
|
32
|
+
return writeRecord("memory.write", options, bounded);
|
|
33
|
+
case "knowledge.write":
|
|
34
|
+
return writeRecord("knowledge.write", options, bounded);
|
|
35
|
+
case "project.detect":
|
|
36
|
+
return projectDetect(classification, cwd);
|
|
37
|
+
case "project.create":
|
|
38
|
+
return projectCreate(classification, options);
|
|
39
|
+
case "project.update":
|
|
40
|
+
return projectUpdate(classification, options, cwd);
|
|
41
|
+
default:
|
|
42
|
+
return operationResult(operation, `unsupported operation '${operation}'`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=record-operations.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { sameProject } from "@ocean/core/shared/paths.js";
|
|
2
|
+
// Metadata-first task linking. No body reads, embeddings, or fuzzy guesses. A candidate
|
|
3
|
+
// only gains points from explicit metadata supplied by the caller, then stable recency.
|
|
4
|
+
export function rankTaskCandidates(candidates, request) {
|
|
5
|
+
const wantedKeywords = new Set((request.keywords ?? []).map((value) => value.toLowerCase()));
|
|
6
|
+
const wantedRelationships = new Set(request.relationshipIds ?? []);
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
const scored = candidates.map((candidate) => {
|
|
9
|
+
const reasons = [];
|
|
10
|
+
let score = 0;
|
|
11
|
+
if (request.projectId &&
|
|
12
|
+
sameProject(candidate.projectId, request.projectId)) {
|
|
13
|
+
score += 100;
|
|
14
|
+
reasons.push("project match");
|
|
15
|
+
}
|
|
16
|
+
if (request.state && candidate.state === request.state) {
|
|
17
|
+
score += 30;
|
|
18
|
+
reasons.push("state match");
|
|
19
|
+
}
|
|
20
|
+
const relationshipMatches = candidate.relationships.filter((id) => wantedRelationships.has(id));
|
|
21
|
+
if (relationshipMatches.length) {
|
|
22
|
+
score += 80 * relationshipMatches.length;
|
|
23
|
+
reasons.push("explicit relationship match");
|
|
24
|
+
}
|
|
25
|
+
const keywordMatches = candidate.keywords.filter((keyword) => wantedKeywords.has(keyword.toLowerCase()));
|
|
26
|
+
if (keywordMatches.length) {
|
|
27
|
+
score += 10 * keywordMatches.length;
|
|
28
|
+
reasons.push("keyword match");
|
|
29
|
+
}
|
|
30
|
+
const ageDays = Math.max(0, (now - Date.parse(candidate.updatedAt)) / 86_400_000);
|
|
31
|
+
if (Number.isFinite(ageDays)) {
|
|
32
|
+
score += Math.max(0, 5 - Math.floor(ageDays / 7));
|
|
33
|
+
reasons.push("recency considered");
|
|
34
|
+
}
|
|
35
|
+
return { ...candidate, score, reasons };
|
|
36
|
+
});
|
|
37
|
+
scored.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
|
|
38
|
+
return scored.slice(0, Math.max(1, Math.min(20, request.limit ?? 20)));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=task-linker.js.map
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TASK_ID } from "@ocean/core/domain/tasks/task-id.js";
|
|
4
|
+
import { atomicWrite, FRONTMATTER_READ_BYTES, } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
6
|
+
import { oceanPath, PROJECTS_DIR, projectSubdir, } from "@ocean/core/shared/paths.js";
|
|
7
|
+
import { withNextTaskId } from "@ocean/core/shared/project-resolution.js";
|
|
8
|
+
import { resolveTaskReference, } from "@ocean/core/shared/task-reference.js";
|
|
9
|
+
import { completeTask } from "../tasks/archive-tasks.js";
|
|
10
|
+
import { taskRecord, validTaskTitle } from "../tasks/new-task.js";
|
|
11
|
+
import { approvalMatchesTarget, operationResult, selectFields, shapeRecords, validateTaskIdentifier, validateWriteTarget, } from "./operation-contract.js";
|
|
12
|
+
import { crossProjectGuard, taskRoot, } from "./operation-scope.js";
|
|
13
|
+
import { freshnessFor, readFrontmatterFile, relativeToOcean, requireAbsentTarget, } from "./record-io.js";
|
|
14
|
+
const TASK_FIELDS = ["id", "title", "state", "goal", "priority", "updated_at"];
|
|
15
|
+
const MUTABLE_TASK_FIELDS = [
|
|
16
|
+
"state",
|
|
17
|
+
"goal",
|
|
18
|
+
"priority",
|
|
19
|
+
"updated_at",
|
|
20
|
+
"next_action",
|
|
21
|
+
];
|
|
22
|
+
export async function taskGet(classification, budget, options, cwd) {
|
|
23
|
+
const identifier = validateTaskIdentifier(classification.identifier);
|
|
24
|
+
if (!identifier.valid)
|
|
25
|
+
return operationResult("task.get", identifier.reason);
|
|
26
|
+
const project = await crossProjectGuard(options, cwd);
|
|
27
|
+
if (!project.ok)
|
|
28
|
+
return operationResult("task.get", project.reason);
|
|
29
|
+
const resolved = await activeTask(identifier.value, project.projectId);
|
|
30
|
+
if (!resolved.ok)
|
|
31
|
+
return operationResult("task.get", resolved.reason);
|
|
32
|
+
const file = resolved.location.file;
|
|
33
|
+
const record = await readFrontmatterFile(file);
|
|
34
|
+
if (!record)
|
|
35
|
+
return operationResult("task.get", `task ${identifier.value} was not found in project '${project.projectId}'`);
|
|
36
|
+
if (record.size > budget.maxBytes) {
|
|
37
|
+
return operationResult("task.get", `task ${identifier.value} is ${record.size} bytes, budget.maxBytes allows ${budget.maxBytes} — refusing to silently truncate`, { violations: ["max-bytes-exceeded"] });
|
|
38
|
+
}
|
|
39
|
+
const shaped = {
|
|
40
|
+
identifier: identifier.value,
|
|
41
|
+
recordType: "task",
|
|
42
|
+
provenance: "task",
|
|
43
|
+
sourcePath: relativeToOcean(file),
|
|
44
|
+
freshness: freshnessFor(record.mtimeMs),
|
|
45
|
+
confidence: classification.confidence,
|
|
46
|
+
selectionReason: `exact task record requested by identifier ${identifier.value}`,
|
|
47
|
+
fields: selectFields(record.fields, TASK_FIELDS),
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
operation: "task.get",
|
|
51
|
+
ok: true,
|
|
52
|
+
reason: `task ${identifier.value} selected`,
|
|
53
|
+
records: [shaped],
|
|
54
|
+
violations: [],
|
|
55
|
+
written: null,
|
|
56
|
+
packet: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function taskList(classification, budget, options, cwd) {
|
|
60
|
+
const project = await crossProjectGuard(options, cwd);
|
|
61
|
+
if (!project.ok)
|
|
62
|
+
return operationResult("task.list", project.reason);
|
|
63
|
+
const root = taskRoot(project.projectId);
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return operationResult("task.list", `no task directory for project '${project.projectId}'`);
|
|
70
|
+
}
|
|
71
|
+
const records = [];
|
|
72
|
+
const violations = [];
|
|
73
|
+
let bytes = 0;
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry.isDirectory() || entry.name === "archive")
|
|
76
|
+
continue;
|
|
77
|
+
if (!TASK_ID.test(entry.name))
|
|
78
|
+
continue;
|
|
79
|
+
const file = path.join(root, entry.name, "task.md");
|
|
80
|
+
const record = await readFrontmatterFile(file);
|
|
81
|
+
if (!record)
|
|
82
|
+
continue;
|
|
83
|
+
bytes += Math.min(record.size, FRONTMATTER_READ_BYTES);
|
|
84
|
+
if (bytes > budget.maxBytes) {
|
|
85
|
+
violations.push(`max-bytes-exceeded: stopped listing at ${bytes} bytes (budget ${budget.maxBytes})`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
records.push({
|
|
89
|
+
identifier: record.fields.id ?? entry.name,
|
|
90
|
+
recordType: "task",
|
|
91
|
+
provenance: "task",
|
|
92
|
+
sourcePath: relativeToOcean(file),
|
|
93
|
+
freshness: freshnessFor(record.mtimeMs),
|
|
94
|
+
confidence: classification.confidence,
|
|
95
|
+
selectionReason: `live task in project '${project.projectId}'`,
|
|
96
|
+
fields: selectFields(record.fields, TASK_FIELDS),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const shaped = shapeRecords(records, budget.maxFiles);
|
|
100
|
+
return {
|
|
101
|
+
operation: "task.list",
|
|
102
|
+
ok: true,
|
|
103
|
+
reason: `${shaped.records.length} task(s) listed for project '${project.projectId}'`,
|
|
104
|
+
records: shaped.records,
|
|
105
|
+
violations: [...violations, ...shaped.violations],
|
|
106
|
+
written: null,
|
|
107
|
+
packet: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Resolves the task inside the active project only. A prefix that belongs to another project
|
|
111
|
+
// gets the same refusal as any cross-project request.
|
|
112
|
+
async function activeTask(id, projectId) {
|
|
113
|
+
try {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
location: await resolveTaskReference(id, {
|
|
117
|
+
projectId,
|
|
118
|
+
acrossProjects: false,
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
const foreign = /^\S+ belongs to (\S+),/.exec(message);
|
|
125
|
+
if (foreign)
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: `cross-project request refused: active project is '${projectId}', request targeted '${foreign[1]}'`,
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: /is not a task in|no project uses prefix/.test(message)
|
|
133
|
+
? `task ${id} was not found in project '${projectId}'`
|
|
134
|
+
: message,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function taskCreate(classification, options, cwd) {
|
|
139
|
+
const project = await crossProjectGuard(options, cwd);
|
|
140
|
+
if (!project.ok)
|
|
141
|
+
return operationResult("task.create", project.reason);
|
|
142
|
+
const title = classification.identifier?.trim();
|
|
143
|
+
if (!validTaskTitle(title))
|
|
144
|
+
return operationResult("task.create", "write refused: an explicit single-line task title is required");
|
|
145
|
+
// Resolve the folder once so the id and the write target can never disagree.
|
|
146
|
+
const folder = projectSubdir(oceanPath(PROJECTS_DIR), project.projectId);
|
|
147
|
+
const today = localDate();
|
|
148
|
+
const objective = (options.content ?? title).trim().replace(/[\r\n]+/g, " ");
|
|
149
|
+
// The approval is bound to the path of the id that was predicted when it was granted. The id
|
|
150
|
+
// is issued under the projects lock, so a creator that lost the race is refused (and consumes
|
|
151
|
+
// nothing) instead of writing under an id nobody approved.
|
|
152
|
+
return withNextTaskId(project.projectId, async (id) => {
|
|
153
|
+
const target = validateWriteTarget(PROJECTS_DIR, folder, "tasks", id, "task.md");
|
|
154
|
+
if (!target.valid)
|
|
155
|
+
return operationResult("task.create", target.reason);
|
|
156
|
+
const approvalCheck = approvalMatchesTarget(options.approval, target.value);
|
|
157
|
+
if (!approvalCheck.ok)
|
|
158
|
+
return operationResult("task.create", `write refused: the next id is now ${id}; the approval covered ${options.approval?.target} — request a new approval`);
|
|
159
|
+
const absent = await requireAbsentTarget(target.value);
|
|
160
|
+
if (!absent.ok)
|
|
161
|
+
return operationResult("task.create", absent.reason);
|
|
162
|
+
const source = taskRecord({
|
|
163
|
+
id,
|
|
164
|
+
title,
|
|
165
|
+
project: project.projectId,
|
|
166
|
+
objective,
|
|
167
|
+
today,
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
await mkdir(path.dirname(target.value), { recursive: true });
|
|
171
|
+
const bytes = await atomicWrite(target.value, source);
|
|
172
|
+
return {
|
|
173
|
+
operation: "task.create",
|
|
174
|
+
ok: true,
|
|
175
|
+
reason: `${id} created for project '${project.projectId}'`,
|
|
176
|
+
records: [],
|
|
177
|
+
violations: [],
|
|
178
|
+
written: { sourcePath: relativeToOcean(target.value), bytes },
|
|
179
|
+
packet: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return operationResult("task.create", `task creation failed atomically, no partial content left: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export async function taskUpdate(classification, budget, options, cwd) {
|
|
188
|
+
const identifier = validateTaskIdentifier(classification.identifier);
|
|
189
|
+
if (!identifier.valid)
|
|
190
|
+
return operationResult("task.update", identifier.reason);
|
|
191
|
+
const project = await crossProjectGuard(options, cwd);
|
|
192
|
+
if (!project.ok)
|
|
193
|
+
return operationResult("task.update", project.reason);
|
|
194
|
+
const patch = options.patch ?? {};
|
|
195
|
+
const patchKeys = Object.keys(patch);
|
|
196
|
+
if (patchKeys.length === 0)
|
|
197
|
+
return operationResult("task.update", "write refused: no explicit field patch supplied");
|
|
198
|
+
const unknownField = patchKeys.find((key) => !MUTABLE_TASK_FIELDS.includes(key));
|
|
199
|
+
if (unknownField)
|
|
200
|
+
return operationResult("task.update", `write refused: field '${unknownField}' is not an updatable task field`);
|
|
201
|
+
const resolved = await activeTask(identifier.value, project.projectId);
|
|
202
|
+
if (!resolved.ok || resolved.location.archived)
|
|
203
|
+
return operationResult("task.update", resolved.ok
|
|
204
|
+
? `task ${identifier.value} was not found in project '${project.projectId}'`
|
|
205
|
+
: resolved.reason);
|
|
206
|
+
const target = validateWriteTarget(PROJECTS_DIR, projectSubdir(oceanPath(PROJECTS_DIR), project.projectId), "tasks", resolved.location.id, "task.md");
|
|
207
|
+
if (!target.valid)
|
|
208
|
+
return operationResult("task.update", target.reason);
|
|
209
|
+
const approvalCheck = approvalMatchesTarget(options.approval, target.value);
|
|
210
|
+
if (!approvalCheck.ok)
|
|
211
|
+
return operationResult("task.update", approvalCheck.reason);
|
|
212
|
+
let source;
|
|
213
|
+
try {
|
|
214
|
+
source = await readFile(target.value, "utf8");
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return operationResult("task.update", `task ${identifier.value} was not found in project '${project.projectId}'`);
|
|
218
|
+
}
|
|
219
|
+
if (Buffer.byteLength(source, "utf8") > budget.maxBytes) {
|
|
220
|
+
return operationResult("task.update", `task ${identifier.value} exceeds budget.maxBytes (${budget.maxBytes}) — refusing to rewrite a record it cannot fully read`, { violations: ["max-bytes-exceeded"] });
|
|
221
|
+
}
|
|
222
|
+
let updated = source;
|
|
223
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
224
|
+
if (value.includes("\n"))
|
|
225
|
+
return operationResult("task.update", `write refused: value for '${key}' must be a single line`);
|
|
226
|
+
const pattern = new RegExp(`^${key}:.*$`, "m");
|
|
227
|
+
updated = pattern.test(updated)
|
|
228
|
+
? updated.replace(pattern, `${key}: ${value}`)
|
|
229
|
+
: updated;
|
|
230
|
+
}
|
|
231
|
+
if (updated === source)
|
|
232
|
+
return operationResult("task.update", "write refused: patch did not match any existing frontmatter field");
|
|
233
|
+
const bytes = await atomicWrite(target.value, updated);
|
|
234
|
+
return {
|
|
235
|
+
operation: "task.update",
|
|
236
|
+
ok: true,
|
|
237
|
+
reason: `task ${identifier.value} updated (${patchKeys.join(", ")})`,
|
|
238
|
+
records: [],
|
|
239
|
+
violations: [],
|
|
240
|
+
written: { sourcePath: relativeToOcean(target.value), bytes },
|
|
241
|
+
packet: null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
export async function taskCompleteOperation(classification, _budget, options, cwd) {
|
|
245
|
+
const identifier = validateTaskIdentifier(classification.identifier);
|
|
246
|
+
if (!identifier.valid)
|
|
247
|
+
return operationResult("task.complete", identifier.reason);
|
|
248
|
+
const project = await crossProjectGuard(options, cwd);
|
|
249
|
+
if (!project.ok)
|
|
250
|
+
return operationResult("task.complete", project.reason);
|
|
251
|
+
const resolved = await activeTask(identifier.value, project.projectId);
|
|
252
|
+
if (!resolved.ok || resolved.location.archived)
|
|
253
|
+
return operationResult("task.complete", resolved.ok
|
|
254
|
+
? `task ${identifier.value} was not found in project '${project.projectId}'`
|
|
255
|
+
: resolved.reason);
|
|
256
|
+
const target = validateWriteTarget(PROJECTS_DIR, projectSubdir(oceanPath(PROJECTS_DIR), project.projectId), "tasks", resolved.location.id, "task.md");
|
|
257
|
+
if (!target.valid)
|
|
258
|
+
return operationResult("task.complete", target.reason);
|
|
259
|
+
const approvalCheck = approvalMatchesTarget(options.approval, target.value);
|
|
260
|
+
if (!approvalCheck.ok)
|
|
261
|
+
return operationResult("task.complete", approvalCheck.reason);
|
|
262
|
+
try {
|
|
263
|
+
// Reuses the existing governed completion path: it refuses a task with unchecked work.
|
|
264
|
+
const result = await completeTask(resolved.location.id, taskRoot(project.projectId));
|
|
265
|
+
return {
|
|
266
|
+
operation: "task.complete",
|
|
267
|
+
ok: true,
|
|
268
|
+
reason: `task ${identifier.value} completed (state: ${result.state})`,
|
|
269
|
+
records: [],
|
|
270
|
+
violations: [],
|
|
271
|
+
written: { sourcePath: relativeToOcean(target.value), bytes: 0 },
|
|
272
|
+
packet: null,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
return operationResult("task.complete", error instanceof Error ? error.message : String(error));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=task-operations.js.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validateBudget } from "@ocean/context/context/context-ladder.js";
|
|
4
|
+
import { engineRoot, oceanRoot, resolveWithin, } from "@ocean/core/shared/paths.js";
|
|
5
|
+
import { OPERATION_KIND, } from "./operation-contract.js";
|
|
6
|
+
const DEFAULT_GRANT_TTL_MS = 15 * 60 * 1000;
|
|
7
|
+
// A grant is bound to the exact scope it was granted for. Any change to the action, the
|
|
8
|
+
// resolved target, the record identifier, or the project produces a different hash, so a
|
|
9
|
+
// reused grant cannot silently cover a different write.
|
|
10
|
+
export function computeScopeHash(scope) {
|
|
11
|
+
const canonical = JSON.stringify({
|
|
12
|
+
action: scope.action,
|
|
13
|
+
target: path.resolve(scope.target),
|
|
14
|
+
identifier: scope.identifier ?? null,
|
|
15
|
+
projectId: scope.projectId ?? null,
|
|
16
|
+
});
|
|
17
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
18
|
+
}
|
|
19
|
+
export function createGrant(sessionId, scope, options = {}) {
|
|
20
|
+
const now = options.now ?? Date.now();
|
|
21
|
+
return {
|
|
22
|
+
grantId: randomUUID(),
|
|
23
|
+
sessionId,
|
|
24
|
+
action: scope.action,
|
|
25
|
+
target: path.resolve(scope.target),
|
|
26
|
+
scopeHash: computeScopeHash(scope),
|
|
27
|
+
grantedAt: new Date(now).toISOString(),
|
|
28
|
+
expiresAt: new Date(now + (options.ttlMs ?? DEFAULT_GRANT_TTL_MS)).toISOString(),
|
|
29
|
+
revokedAt: null,
|
|
30
|
+
consumed: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function revokeGrant(grant, now = Date.now()) {
|
|
34
|
+
return { ...grant, revokedAt: new Date(now).toISOString() };
|
|
35
|
+
}
|
|
36
|
+
export function consentStateOf(grant, now = Date.now()) {
|
|
37
|
+
if (!grant)
|
|
38
|
+
return "none";
|
|
39
|
+
if (grant.revokedAt)
|
|
40
|
+
return "revoked";
|
|
41
|
+
if (Date.parse(grant.expiresAt) <= now)
|
|
42
|
+
return "expired";
|
|
43
|
+
return "granted";
|
|
44
|
+
}
|
|
45
|
+
function decision(request, code, reason, consent, scopeHash, approval = null) {
|
|
46
|
+
return {
|
|
47
|
+
allowed: code === "allowed",
|
|
48
|
+
code,
|
|
49
|
+
reason,
|
|
50
|
+
consent,
|
|
51
|
+
approval,
|
|
52
|
+
audit: {
|
|
53
|
+
action: request.scope.action,
|
|
54
|
+
target: path.resolve(request.scope.target),
|
|
55
|
+
sessionId: request.sessionId,
|
|
56
|
+
scopeHash,
|
|
57
|
+
intent: request.classification.intent,
|
|
58
|
+
confidence: request.classification.confidence,
|
|
59
|
+
code,
|
|
60
|
+
decidedAt: new Date(request.now ?? Date.now()).toISOString(),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// The one decision function. Read operations pass straight through (they are bounded
|
|
65
|
+
// elsewhere); every write, execution, and provider invocation is evaluated here.
|
|
66
|
+
export function evaluateGuard(request) {
|
|
67
|
+
const now = request.now ?? Date.now();
|
|
68
|
+
const scopeHash = computeScopeHash(request.scope);
|
|
69
|
+
const consent = consentStateOf(request.grant, now);
|
|
70
|
+
const budgetCheck = validateBudget(request.budget);
|
|
71
|
+
if (!budgetCheck.valid)
|
|
72
|
+
return decision(request, "invalid-budget", `denied: ${budgetCheck.reason}`, consent, scopeHash);
|
|
73
|
+
const isRecordOperation = request.scope.action in OPERATION_KIND;
|
|
74
|
+
const needsApproval = !isRecordOperation ||
|
|
75
|
+
OPERATION_KIND[request.scope.action] === "write";
|
|
76
|
+
if (!needsApproval) {
|
|
77
|
+
return decision(request, "allowed", "read operation: no approval required", consent, scopeHash, null);
|
|
78
|
+
}
|
|
79
|
+
if (request.classification.intent === "unknown") {
|
|
80
|
+
return decision(request, "unknown-intent", "denied: unknown intent may never write, execute, or invoke a provider", consent, scopeHash);
|
|
81
|
+
}
|
|
82
|
+
if (request.classification.ambiguityReason) {
|
|
83
|
+
return decision(request, "ambiguous-intent", `denied: ambiguous intent may never write, execute, or invoke a provider (${request.classification.ambiguityReason})`, consent, scopeHash);
|
|
84
|
+
}
|
|
85
|
+
if (request.classification.confidence !== "high") {
|
|
86
|
+
return decision(request, "low-confidence", `denied: confidence '${request.classification.confidence}' is below the required 'high'`, consent, scopeHash);
|
|
87
|
+
}
|
|
88
|
+
if (request.scope.target.includes("\0")) {
|
|
89
|
+
return decision(request, "invalid-target", "denied: target contains a null byte", consent, scopeHash);
|
|
90
|
+
}
|
|
91
|
+
// A record write must resolve inside the private Ocean root and never inside the public
|
|
92
|
+
// engine package, whatever path the caller passed. resolveWithin (paths.js) rejects both a
|
|
93
|
+
// lexical escape and a symlink that resolves outside the given root, so a symlink planted
|
|
94
|
+
// inside the Ocean root pointing outside it can't be used to land a write elsewhere.
|
|
95
|
+
if (isRecordOperation) {
|
|
96
|
+
const resolved = path.resolve(request.scope.target);
|
|
97
|
+
try {
|
|
98
|
+
resolveWithin(oceanRoot(), resolved);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return decision(request, "invalid-target", "denied: target escapes the Ocean root", consent, scopeHash);
|
|
102
|
+
}
|
|
103
|
+
let insideEngine = true;
|
|
104
|
+
try {
|
|
105
|
+
resolveWithin(engineRoot(), resolved);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
insideEngine = false;
|
|
109
|
+
}
|
|
110
|
+
if (insideEngine) {
|
|
111
|
+
return decision(request, "invalid-target", "denied: refusing to write private Ocean content inside the public engine package", consent, scopeHash);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (consent === "none")
|
|
115
|
+
return decision(request, "no-consent", "denied: no explicit approval grant was supplied — approval is never inferred from wording", consent, scopeHash);
|
|
116
|
+
if (consent === "revoked")
|
|
117
|
+
return decision(request, "consent-revoked", "denied: the approval grant was revoked", consent, scopeHash);
|
|
118
|
+
if (consent === "expired")
|
|
119
|
+
return decision(request, "consent-expired", "denied: the approval grant has expired", consent, scopeHash);
|
|
120
|
+
const grant = request.grant;
|
|
121
|
+
if (grant.consumed)
|
|
122
|
+
return decision(request, "consent-consumed", "denied: the approval grant was already used and is not reusable", consent, scopeHash);
|
|
123
|
+
if (grant.sessionId !== request.sessionId) {
|
|
124
|
+
return decision(request, "wrong-session", `denied: the approval grant belongs to session ${grant.sessionId}, not ${request.sessionId} — approval is never inherited`, consent, scopeHash);
|
|
125
|
+
}
|
|
126
|
+
if (grant.action !== request.scope.action) {
|
|
127
|
+
return decision(request, "wrong-action", `denied: the approval grant covers '${grant.action}', not '${request.scope.action}'`, consent, scopeHash);
|
|
128
|
+
}
|
|
129
|
+
if (path.resolve(grant.target) !== path.resolve(request.scope.target)) {
|
|
130
|
+
return decision(request, "wrong-target", `denied: the approval grant covers a different target`, consent, scopeHash);
|
|
131
|
+
}
|
|
132
|
+
if (grant.scopeHash !== scopeHash) {
|
|
133
|
+
return decision(request, "scope-changed", "denied: the request scope changed after the approval was granted", consent, scopeHash);
|
|
134
|
+
}
|
|
135
|
+
const approval = isRecordOperation
|
|
136
|
+
? {
|
|
137
|
+
approved: true,
|
|
138
|
+
operation: request.scope.action,
|
|
139
|
+
target: path.resolve(request.scope.target),
|
|
140
|
+
}
|
|
141
|
+
: null;
|
|
142
|
+
return decision(request, "allowed", `approved: grant ${grant.grantId} covers this exact action, target, and scope`, consent, scopeHash, approval);
|
|
143
|
+
}
|
|
144
|
+
// Marks a grant used. A grant is single-use, so an approved write cannot be replayed.
|
|
145
|
+
export function consumeGrant(grant) {
|
|
146
|
+
return { ...grant, consumed: true };
|
|
147
|
+
}
|
|
148
|
+
// The single sanctioned write path: guard first, operation second. A caller that skips this
|
|
149
|
+
// still hits slice 7's own approval check, which only accepts the approval object the guard
|
|
150
|
+
// produces — there is no path to a durable write that bypasses both.
|
|
151
|
+
export async function guardedRunOperation(request, run) {
|
|
152
|
+
const verdict = evaluateGuard(request);
|
|
153
|
+
if (!verdict.allowed)
|
|
154
|
+
return { decision: verdict, result: null };
|
|
155
|
+
return { decision: verdict, result: await run(verdict.approval) };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=write-guard.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { redactRuntimeText } from "@ocean/core/domain/redaction/redaction.js";
|
|
3
|
+
import { truncateUtf8 } from "@ocean/core/shared/fs-utils.js";
|
|
4
|
+
// Pure formatting for the headless runAgent prompt: every section is text built from data
|
|
5
|
+
// the caller already loaded, and each section's byte size is measured so the context_cost
|
|
6
|
+
// event can report where the bytes went. No I/O here.
|
|
7
|
+
export const PROMPT_SECTIONS = [
|
|
8
|
+
"request",
|
|
9
|
+
"profile",
|
|
10
|
+
"instructions",
|
|
11
|
+
"skills",
|
|
12
|
+
"facts",
|
|
13
|
+
"handoff",
|
|
14
|
+
"context",
|
|
15
|
+
];
|
|
16
|
+
export const PROMOTED_SKILL_MAX_BYTES = 4_096;
|
|
17
|
+
export const SKILL_DESCRIPTION_MAX_BYTES = 300;
|
|
18
|
+
const SEPARATOR = "\n\n";
|
|
19
|
+
// Joins the non-empty sections in PROMPT_SECTIONS order. `separators` counts the join bytes,
|
|
20
|
+
// so the breakdown always sums exactly to `bytes`.
|
|
21
|
+
export function assemblePrompt(sections) {
|
|
22
|
+
const breakdown = {};
|
|
23
|
+
const present = [];
|
|
24
|
+
for (const name of PROMPT_SECTIONS) {
|
|
25
|
+
const text = sections[name];
|
|
26
|
+
breakdown[name] = text ? Buffer.byteLength(text) : 0;
|
|
27
|
+
if (text)
|
|
28
|
+
present.push(text);
|
|
29
|
+
}
|
|
30
|
+
breakdown.separators = present.length
|
|
31
|
+
? Buffer.byteLength(SEPARATOR) * (present.length - 1)
|
|
32
|
+
: 0;
|
|
33
|
+
const prompt = present.join(SEPARATOR);
|
|
34
|
+
return { prompt, bytes: Buffer.byteLength(prompt), breakdown };
|
|
35
|
+
}
|
|
36
|
+
const collapse = (value) => value.replace(/\s*\n\s*/g, " ").trim();
|
|
37
|
+
const list = (values) => JSON.stringify(values);
|
|
38
|
+
// The effective profile as compact `key: value` lines, omitting empty and default fields.
|
|
39
|
+
// writePolicy is always emitted: it is a safety statement even at its default.
|
|
40
|
+
export function formatProfileContract(profile, run) {
|
|
41
|
+
const lines = [
|
|
42
|
+
`profile: ${collapse(profile.name)}`,
|
|
43
|
+
`role: ${collapse(profile.role)}`,
|
|
44
|
+
`provider: ${profile.provider}`,
|
|
45
|
+
];
|
|
46
|
+
if (profile.model !== "provider-managed")
|
|
47
|
+
lines.push(`model: ${collapse(profile.model)}`);
|
|
48
|
+
const client = profile.clients[profile.provider];
|
|
49
|
+
if (client) {
|
|
50
|
+
if (client.capabilities.length)
|
|
51
|
+
lines.push(`client.capabilities: ${list(client.capabilities)}`);
|
|
52
|
+
if (client.limitations.length)
|
|
53
|
+
lines.push(`client.limitations: ${list(client.limitations)}`);
|
|
54
|
+
if (client.model)
|
|
55
|
+
lines.push(`client.model: ${collapse(client.model)}`);
|
|
56
|
+
if (client.profile)
|
|
57
|
+
lines.push(`client.profile: ${collapse(client.profile)}`);
|
|
58
|
+
if (client.mode)
|
|
59
|
+
lines.push(`client.mode: ${collapse(client.mode)}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(`writePolicy: ${profile.writePolicy}`);
|
|
62
|
+
if (profile.allowedPaths.length)
|
|
63
|
+
lines.push(`allowedPaths: ${list(profile.allowedPaths)}`);
|
|
64
|
+
if (profile.allowedCommands.length)
|
|
65
|
+
lines.push(`allowedCommands: ${list(profile.allowedCommands)}`);
|
|
66
|
+
if (profile.verification.commands.length)
|
|
67
|
+
lines.push(`verification: ${list(profile.verification.commands)}`);
|
|
68
|
+
if (profile.governance?.approvalRequired)
|
|
69
|
+
lines.push("approvalRequired: true");
|
|
70
|
+
if (!profile.memory.enabled)
|
|
71
|
+
lines.push("memoryScope: disabled");
|
|
72
|
+
else if (profile.memory.scope !== "profile")
|
|
73
|
+
lines.push(`memoryScope: ${collapse(profile.memory.scope)}`);
|
|
74
|
+
if (run.taskId)
|
|
75
|
+
lines.push(`taskId: ${collapse(run.taskId)}`);
|
|
76
|
+
if (run.handoffId)
|
|
77
|
+
lines.push(`handoffId: ${collapse(run.handoffId)}`);
|
|
78
|
+
return `## Effective Ocean profile\n${lines.join("\n")}`;
|
|
79
|
+
}
|
|
80
|
+
// One line per skill: name, one-line description, and the SKILL.md path the provider reads
|
|
81
|
+
// before applying it (progressive disclosure: bodies are never inlined).
|
|
82
|
+
export function formatSkillIndex(entries) {
|
|
83
|
+
if (!entries.length)
|
|
84
|
+
return "";
|
|
85
|
+
const lines = entries.map((entry) => `- ${entry.name}: ${truncateUtf8(entry.description.replace(/\s+/g, " ").trim(), SKILL_DESCRIPTION_MAX_BYTES)} (${entry.path})`);
|
|
86
|
+
return `## Ocean skills\nSkill bodies are not inlined; read a skill's SKILL.md at the path shown before applying it.\n${lines.join("\n")}`;
|
|
87
|
+
}
|
|
88
|
+
// Lessons have no SKILL.md, so their (budget-bounded) text stays inline.
|
|
89
|
+
export function formatActiveLessons(lessons) {
|
|
90
|
+
return lessons
|
|
91
|
+
.map((lesson) => `## Lesson: ${redactRuntimeText(lesson.title)} [${lesson.id}]\n${redactRuntimeText(lesson.body)}`)
|
|
92
|
+
.join("\n\n");
|
|
93
|
+
}
|
|
94
|
+
// The directories of referenced files that lie outside cwd, deduped in order. The prompt
|
|
95
|
+
// points the provider at these files (skill folders, context references), so a provider
|
|
96
|
+
// whose file tools are confined to its workspace must be granted them explicitly.
|
|
97
|
+
export function readDirectoriesOutside(cwd, files) {
|
|
98
|
+
const root = path.resolve(cwd);
|
|
99
|
+
const directories = new Set();
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
const directory = path.dirname(path.resolve(root, file));
|
|
102
|
+
const relative = path.relative(root, directory);
|
|
103
|
+
const inside = relative === "" ||
|
|
104
|
+
(relative !== ".." &&
|
|
105
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
106
|
+
!path.isAbsolute(relative));
|
|
107
|
+
if (!inside)
|
|
108
|
+
directories.add(directory);
|
|
109
|
+
}
|
|
110
|
+
return [...directories];
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=prompt-assembly.js.map
|