@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,317 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { marketplaceIndexSchema } from "@ocean/core/domain/capabilities/capability-manifest.js";
|
|
4
|
+
import { engineRoot } from "@ocean/core/shared/paths.js";
|
|
5
|
+
import { scanText } from "../security/description-scan.js";
|
|
6
|
+
import { engineIndexFile } from "./mcp-connection.js";
|
|
7
|
+
import { literalArguments, planMcpLaunch, } from "./mcp-launch.js";
|
|
8
|
+
export const GATEWAY_CAPABILITY = "dev.ocean/mcp-gateway";
|
|
9
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
10
|
+
function namespaceFor(capability, taken) {
|
|
11
|
+
const clean = (value) => value.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
|
|
12
|
+
const short = clean(capability.split("/").at(-1) ?? capability);
|
|
13
|
+
let prefix = taken.has(short) ? clean(capability) : short;
|
|
14
|
+
for (let suffix = 2; taken.has(prefix); suffix++)
|
|
15
|
+
prefix = `${clean(capability)}-${suffix}`;
|
|
16
|
+
taken.add(prefix);
|
|
17
|
+
return prefix;
|
|
18
|
+
}
|
|
19
|
+
// The servers the gateway routes: the engine's first-party ones, then every MCP server installed
|
|
20
|
+
// from the marketplace, each planned by the same launcher as `ocean market exec` — declared
|
|
21
|
+
// permissions, declared environment only. A third-party server joins only by
|
|
22
|
+
// being installed; anything that cannot be launched here is reported, not fatal.
|
|
23
|
+
export async function gatewayMembers(options) {
|
|
24
|
+
const members = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
const taken = new Set();
|
|
27
|
+
const notFirstParty = [];
|
|
28
|
+
const index = marketplaceIndexSchema.parse(JSON.parse(await readFile(options.indexFile ?? engineIndexFile(), "utf8")));
|
|
29
|
+
for (const manifest of index.capabilities) {
|
|
30
|
+
if (manifest.kind !== "mcp" || manifest.name === GATEWAY_CAPABILITY)
|
|
31
|
+
continue;
|
|
32
|
+
const item = manifest.spec.packages?.[0];
|
|
33
|
+
if (item?.registryType !== "ocean-engine") {
|
|
34
|
+
notFirstParty.push(manifest.name);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
members.push({
|
|
38
|
+
capability: manifest.name,
|
|
39
|
+
prefix: namespaceFor(manifest.name, taken),
|
|
40
|
+
command: process.execPath,
|
|
41
|
+
args: [
|
|
42
|
+
path.join(engineRoot(), "dist", "main.js"),
|
|
43
|
+
...literalArguments(item.packageArguments, `${manifest.name} packageArguments`),
|
|
44
|
+
],
|
|
45
|
+
cwd: engineRoot(),
|
|
46
|
+
env: {},
|
|
47
|
+
inheritEnv: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
let files = [];
|
|
51
|
+
try {
|
|
52
|
+
files = (await readdir(options.serversRoot))
|
|
53
|
+
.filter((file) => file.endsWith(".json"))
|
|
54
|
+
.sort();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Nothing installed yet.
|
|
58
|
+
}
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
let installed;
|
|
61
|
+
try {
|
|
62
|
+
installed = JSON.parse(await readFile(path.join(options.serversRoot, file), "utf8"));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
skipped.push({ capability: file, reason: errorMessage(error) });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const name = installed.capability;
|
|
69
|
+
// An installed copy of an engine server runs the same code as the member already routed.
|
|
70
|
+
if (name === GATEWAY_CAPABILITY ||
|
|
71
|
+
members.some((member) => member.capability === name))
|
|
72
|
+
continue;
|
|
73
|
+
const stateDir = path.join(options.stateRoot, path.basename(file, ".json"));
|
|
74
|
+
try {
|
|
75
|
+
const launch = planMcpLaunch(installed, {
|
|
76
|
+
stateDir,
|
|
77
|
+
env: options.env,
|
|
78
|
+
});
|
|
79
|
+
if (launch.transport === "remote") {
|
|
80
|
+
skipped.push({
|
|
81
|
+
capability: name,
|
|
82
|
+
reason: `remote server (${launch.type} ${launch.url}); connect the client to it directly`,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
await mkdir(stateDir, { recursive: true });
|
|
87
|
+
members.push({
|
|
88
|
+
capability: name,
|
|
89
|
+
prefix: namespaceFor(name, taken),
|
|
90
|
+
command: launch.command,
|
|
91
|
+
args: launch.args,
|
|
92
|
+
cwd: launch.cwd,
|
|
93
|
+
env: launch.env,
|
|
94
|
+
inheritEnv: false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
skipped.push({ capability: name, reason: errorMessage(error) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const name of notFirstParty)
|
|
102
|
+
if (!members.some((member) => member.capability === name))
|
|
103
|
+
skipped.push({
|
|
104
|
+
capability: name,
|
|
105
|
+
reason: "not first-party; install it from the marketplace to route it through the gateway",
|
|
106
|
+
});
|
|
107
|
+
return { members, skipped };
|
|
108
|
+
}
|
|
109
|
+
// Every text a model reads for a tool: its name, its description and each key and string inside
|
|
110
|
+
// its input schema (property names, titles, enums and defaults included).
|
|
111
|
+
function toolTexts(tool) {
|
|
112
|
+
const texts = [];
|
|
113
|
+
const walk = (value, at) => {
|
|
114
|
+
if (typeof value === "string")
|
|
115
|
+
texts.push({ at, text: value });
|
|
116
|
+
else if (Array.isArray(value))
|
|
117
|
+
value.forEach((item, index) => {
|
|
118
|
+
walk(item, `${at}[${index}]`);
|
|
119
|
+
});
|
|
120
|
+
else if (value && typeof value === "object")
|
|
121
|
+
for (const [key, item] of Object.entries(value)) {
|
|
122
|
+
texts.push({ at: `${at} key`, text: key });
|
|
123
|
+
walk(item, `${at}.${key}`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
walk(tool.name, "name");
|
|
127
|
+
walk(tool.description, "description");
|
|
128
|
+
walk(tool.inputSchema, "inputSchema");
|
|
129
|
+
return texts;
|
|
130
|
+
}
|
|
131
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
132
|
+
function withTimeout(promise, ms, what) {
|
|
133
|
+
let timer;
|
|
134
|
+
return Promise.race([
|
|
135
|
+
promise,
|
|
136
|
+
new Promise((_, reject) => {
|
|
137
|
+
timer = setTimeout(() => reject(new Error(`${what} within ${ms} ms`)), ms);
|
|
138
|
+
}),
|
|
139
|
+
]).finally(() => clearTimeout(timer));
|
|
140
|
+
}
|
|
141
|
+
class GatewayError extends Error {
|
|
142
|
+
code;
|
|
143
|
+
constructor(code, message) {
|
|
144
|
+
super(message);
|
|
145
|
+
this.code = code;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// One MCP endpoint in front of many servers. Tools are exposed as `<prefix>__<tool>`; a tool
|
|
149
|
+
// whose texts carry injected instructions is hidden, since clients read them as prompt. The
|
|
150
|
+
// gateway adds no authority and removes none: a call reaches its server with the arguments the
|
|
151
|
+
// client sent, so the server's own checks (an approval fingerprint, say) decide as they would
|
|
152
|
+
// on a direct connection, and the client's permission prompt stays the human gate.
|
|
153
|
+
export class McpGateway {
|
|
154
|
+
members;
|
|
155
|
+
options;
|
|
156
|
+
#routes = new Map();
|
|
157
|
+
#tools = [];
|
|
158
|
+
#clients = [];
|
|
159
|
+
#started;
|
|
160
|
+
constructor(members, options) {
|
|
161
|
+
this.members = members;
|
|
162
|
+
this.options = options;
|
|
163
|
+
}
|
|
164
|
+
// Starts every member at once; one that fails or hangs is left out, never the whole gateway.
|
|
165
|
+
start() {
|
|
166
|
+
this.#started ??= this.#connectAll();
|
|
167
|
+
return this.#started;
|
|
168
|
+
}
|
|
169
|
+
async #connectAll() {
|
|
170
|
+
const timeoutMs = this.options.timeoutMs ?? 15_000;
|
|
171
|
+
const clients = this.members.map((member) => this.options.client(member));
|
|
172
|
+
this.#clients.push(...clients);
|
|
173
|
+
const results = await Promise.allSettled(this.members.map(async (member, index) => {
|
|
174
|
+
await withTimeout(clients[index].connect(), timeoutMs, `${member.capability} did not start`);
|
|
175
|
+
return withTimeout(clients[index].listTools(), timeoutMs, `${member.capability} did not list its tools`);
|
|
176
|
+
}));
|
|
177
|
+
results.forEach((result, index) => {
|
|
178
|
+
const member = this.members[index];
|
|
179
|
+
if (result.status === "rejected") {
|
|
180
|
+
this.#log(`${member.capability} left out: ${errorMessage(result.reason)}`);
|
|
181
|
+
clients[index].close();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const tool of result.value)
|
|
185
|
+
this.#expose(member, clients[index], tool);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
#expose(member, client, tool) {
|
|
189
|
+
const findings = toolTexts(tool).flatMap(({ at, text }) => scanText(at, text));
|
|
190
|
+
if (findings.length) {
|
|
191
|
+
this.#log(`${member.capability} tool ${tool.name} hidden: ${findings.map((finding) => `${finding.rule} in ${finding.source} ("${finding.excerpt}")`).join("; ")}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const name = `${member.prefix}__${tool.name}`;
|
|
195
|
+
if (this.#routes.has(name)) {
|
|
196
|
+
this.#log(`${member.capability} lists ${tool.name} twice; kept the first`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.#routes.set(name, { client, tool: tool.name });
|
|
200
|
+
this.#tools.push({ ...tool, name });
|
|
201
|
+
}
|
|
202
|
+
async tools() {
|
|
203
|
+
await this.start();
|
|
204
|
+
return [...this.#tools];
|
|
205
|
+
}
|
|
206
|
+
async call(name, args) {
|
|
207
|
+
await this.start();
|
|
208
|
+
const route = this.#routes.get(name);
|
|
209
|
+
if (!route)
|
|
210
|
+
throw new GatewayError(-32602, `Unknown MCP tool: ${name}`);
|
|
211
|
+
return route.client.forwardToolCall(route.tool, args);
|
|
212
|
+
}
|
|
213
|
+
async handle(request) {
|
|
214
|
+
// MCP 2025-06-18 has no batches; anything but one request object is invalid.
|
|
215
|
+
if (!isRecord(request))
|
|
216
|
+
return {
|
|
217
|
+
jsonrpc: "2.0",
|
|
218
|
+
id: null,
|
|
219
|
+
error: { code: -32600, message: "Invalid Request" },
|
|
220
|
+
};
|
|
221
|
+
// Notifications get no answer.
|
|
222
|
+
if (request.id === undefined)
|
|
223
|
+
return null;
|
|
224
|
+
const id = typeof request.id === "string" || typeof request.id === "number"
|
|
225
|
+
? request.id
|
|
226
|
+
: null;
|
|
227
|
+
return this.#respond(request, id);
|
|
228
|
+
}
|
|
229
|
+
async #respond(request, id) {
|
|
230
|
+
try {
|
|
231
|
+
return { jsonrpc: "2.0", id, result: await this.#answer(request) };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
return {
|
|
235
|
+
jsonrpc: "2.0",
|
|
236
|
+
id,
|
|
237
|
+
error: {
|
|
238
|
+
code: error instanceof GatewayError ? error.code : -32000,
|
|
239
|
+
message: errorMessage(error),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async #answer(request) {
|
|
245
|
+
switch (request.method) {
|
|
246
|
+
case "initialize":
|
|
247
|
+
// Members start while the client finishes its handshake.
|
|
248
|
+
void this.start();
|
|
249
|
+
return {
|
|
250
|
+
protocolVersion: "2025-06-18",
|
|
251
|
+
capabilities: { tools: {} },
|
|
252
|
+
serverInfo: {
|
|
253
|
+
name: this.options.serverName ?? "ocean-gateway",
|
|
254
|
+
version: this.options.version ?? "0.0.0",
|
|
255
|
+
},
|
|
256
|
+
...(this.options.instructions
|
|
257
|
+
? { instructions: this.options.instructions }
|
|
258
|
+
: {}),
|
|
259
|
+
};
|
|
260
|
+
case "ping":
|
|
261
|
+
return {};
|
|
262
|
+
case "tools/list":
|
|
263
|
+
return { tools: await this.tools() };
|
|
264
|
+
case "tools/call": {
|
|
265
|
+
const params = isRecord(request.params) ? request.params : {};
|
|
266
|
+
const args = params.arguments ?? {};
|
|
267
|
+
if (typeof params.name !== "string" || !isRecord(args))
|
|
268
|
+
throw new GatewayError(-32602, "tools/call needs a tool name and an arguments object");
|
|
269
|
+
return this.call(params.name, args);
|
|
270
|
+
}
|
|
271
|
+
default:
|
|
272
|
+
throw new GatewayError(-32601, `Unsupported MCP method: ${request.method ?? ""}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
close() {
|
|
276
|
+
for (const client of this.#clients)
|
|
277
|
+
client.close();
|
|
278
|
+
}
|
|
279
|
+
#log(line) {
|
|
280
|
+
this.options.log?.(line);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Newline-delimited JSON-RPC over stdio. Requests run concurrently, so a slow tool on one server
|
|
284
|
+
// does not hold up another; members are stopped when the client closes the stream.
|
|
285
|
+
export async function serveMcpGateway(gateway, input = process.stdin, output = process.stdout) {
|
|
286
|
+
let buffer = "";
|
|
287
|
+
const write = (response) => {
|
|
288
|
+
if (response)
|
|
289
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
290
|
+
};
|
|
291
|
+
input.setEncoding("utf8");
|
|
292
|
+
input.on("data", (chunk) => {
|
|
293
|
+
buffer += chunk;
|
|
294
|
+
for (let newline = buffer.indexOf("\n"); newline >= 0; newline = buffer.indexOf("\n")) {
|
|
295
|
+
const line = buffer.slice(0, newline).trim();
|
|
296
|
+
buffer = buffer.slice(newline + 1);
|
|
297
|
+
if (!line)
|
|
298
|
+
continue;
|
|
299
|
+
let request;
|
|
300
|
+
try {
|
|
301
|
+
request = JSON.parse(line);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
write({
|
|
305
|
+
jsonrpc: "2.0",
|
|
306
|
+
id: null,
|
|
307
|
+
error: { code: -32700, message: "Parse error" },
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
void gateway.handle(request).then(write);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
await new Promise((resolve) => input.once("end", resolve));
|
|
315
|
+
gateway.close();
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=mcp-gateway.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { PermissionPolicy } from "../security/permissions.js";
|
|
3
|
+
// Where each runner takes its own (runtime) arguments: before the package for npx and uvx,
|
|
4
|
+
// after `run` for docker, so container flags such as `-e` or `-v` reach `docker run`.
|
|
5
|
+
const runners = {
|
|
6
|
+
npm: (identifier, version, runtime) => ({
|
|
7
|
+
command: "npx",
|
|
8
|
+
args: [...runtime, "-y", version ? `${identifier}@${version}` : identifier],
|
|
9
|
+
}),
|
|
10
|
+
pypi: (identifier, version, runtime) => ({
|
|
11
|
+
command: "uvx",
|
|
12
|
+
args: [...runtime, version ? `${identifier}==${version}` : identifier],
|
|
13
|
+
}),
|
|
14
|
+
oci: (identifier, version, runtime) => ({
|
|
15
|
+
command: "docker",
|
|
16
|
+
args: [
|
|
17
|
+
"run",
|
|
18
|
+
"-i",
|
|
19
|
+
"--rm",
|
|
20
|
+
...runtime,
|
|
21
|
+
version ? `${identifier}:${version}` : identifier,
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
export function literalArguments(list, where) {
|
|
26
|
+
if (list === undefined)
|
|
27
|
+
return [];
|
|
28
|
+
if (!Array.isArray(list))
|
|
29
|
+
throw new Error(`${where} must be an array`);
|
|
30
|
+
return list.flatMap((argument) => {
|
|
31
|
+
if (typeof argument?.value !== "string")
|
|
32
|
+
throw new Error(`${where}: only arguments with a literal value are supported`);
|
|
33
|
+
if (argument.type === "named") {
|
|
34
|
+
if (typeof argument.name !== "string")
|
|
35
|
+
throw new Error(`${where}: a named argument needs a name`);
|
|
36
|
+
return [argument.name, argument.value];
|
|
37
|
+
}
|
|
38
|
+
return [argument.value];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Turns an installed server into the exact process (or endpoint) to connect to, checking every
|
|
42
|
+
// step against what the capability declared: the runner must be a declared `exec`, every
|
|
43
|
+
// environment variable a declared `env`, a remote's host a declared `network`. The process gets
|
|
44
|
+
// only those variables (plus PATH so the runner resolves).
|
|
45
|
+
export function planMcpLaunch(installed, options) {
|
|
46
|
+
const home = options.home ?? os.homedir();
|
|
47
|
+
const hostEnv = options.env ?? process.env;
|
|
48
|
+
const policy = new PermissionPolicy(installed.capability, installed.permissions, home);
|
|
49
|
+
const { server } = installed;
|
|
50
|
+
const stdio = server.packages?.find((item) => {
|
|
51
|
+
const transport = item.transport?.type;
|
|
52
|
+
return transport === undefined || transport === "stdio";
|
|
53
|
+
});
|
|
54
|
+
if (!stdio) {
|
|
55
|
+
const remote = server.remotes?.[0];
|
|
56
|
+
if (!remote)
|
|
57
|
+
throw new Error(`${installed.capability} has no stdio package or remote endpoint`);
|
|
58
|
+
const url = new URL(String(remote.url));
|
|
59
|
+
policy.require({ type: "network", target: url.hostname });
|
|
60
|
+
return {
|
|
61
|
+
transport: "remote",
|
|
62
|
+
type: String(remote.type ?? "streamable-http"),
|
|
63
|
+
url: url.href,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const registryType = String(stdio.registryType);
|
|
67
|
+
const runner = runners[registryType];
|
|
68
|
+
if (!runner)
|
|
69
|
+
throw new Error(`${installed.capability}: registry type ${registryType} is not supported`);
|
|
70
|
+
const base = runner(String(stdio.identifier), typeof stdio.version === "string" ? stdio.version : undefined, literalArguments(stdio.runtimeArguments, "runtimeArguments"));
|
|
71
|
+
policy.require({ type: "exec", target: base.command });
|
|
72
|
+
const env = {
|
|
73
|
+
PATH: hostEnv.PATH ?? "/usr/bin:/bin",
|
|
74
|
+
HOME: home,
|
|
75
|
+
};
|
|
76
|
+
const declaredVariables = Array.isArray(stdio.environmentVariables)
|
|
77
|
+
? stdio.environmentVariables
|
|
78
|
+
: [];
|
|
79
|
+
for (const variable of declaredVariables) {
|
|
80
|
+
if (typeof variable?.name !== "string")
|
|
81
|
+
throw new Error(`${installed.capability}: environment variable without a name`);
|
|
82
|
+
policy.require({ type: "env", target: variable.name });
|
|
83
|
+
const value = hostEnv[variable.name] ??
|
|
84
|
+
(typeof variable.default === "string" ? variable.default : undefined);
|
|
85
|
+
if (value !== undefined)
|
|
86
|
+
env[variable.name] = value;
|
|
87
|
+
else if (variable.isRequired === true)
|
|
88
|
+
throw new Error(`${installed.capability} needs ${variable.name} set in the environment`);
|
|
89
|
+
}
|
|
90
|
+
const args = [
|
|
91
|
+
...base.args,
|
|
92
|
+
...literalArguments(stdio.packageArguments, "packageArguments"),
|
|
93
|
+
];
|
|
94
|
+
return {
|
|
95
|
+
transport: "stdio",
|
|
96
|
+
command: base.command,
|
|
97
|
+
args,
|
|
98
|
+
env,
|
|
99
|
+
cwd: options.stateDir,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=mcp-launch.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mcpServerFile } from "../adapters/mcp-adapter.js";
|
|
5
|
+
import { planMcpLaunch } from "./mcp-launch.js";
|
|
6
|
+
// The one way an installed MCP server is started: planned against its declared permissions,
|
|
7
|
+
// then run with stdio passed through, so a client can point at
|
|
8
|
+
// `ocean market exec <name>` as its stdio command.
|
|
9
|
+
export async function runInstalledMcp(name, options) {
|
|
10
|
+
let installed;
|
|
11
|
+
try {
|
|
12
|
+
installed = JSON.parse(await readFile(mcpServerFile(options.serversRoot, name), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error(`${name} is not an installed MCP server`);
|
|
16
|
+
}
|
|
17
|
+
const stateDir = path.join(options.stateRoot, path.basename(mcpServerFile(options.serversRoot, name), ".json"));
|
|
18
|
+
await mkdir(stateDir, { recursive: true });
|
|
19
|
+
const launch = planMcpLaunch(installed, { stateDir });
|
|
20
|
+
if (launch.transport === "remote")
|
|
21
|
+
throw new Error(`${name} is a remote server (${launch.type} ${launch.url}); connect the client to it directly`);
|
|
22
|
+
const child = spawn(launch.command, launch.args, {
|
|
23
|
+
cwd: launch.cwd,
|
|
24
|
+
env: launch.env,
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
});
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
child.once("error", reject);
|
|
29
|
+
child.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=mcp-runner.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const rules = [
|
|
2
|
+
{
|
|
3
|
+
rule: "override-instructions",
|
|
4
|
+
pattern: /\b(ignore|disregard|forget|override)\b[^.\n]{0,40}\b(previous|prior|above|earlier|system|all)\b[^.\n]{0,20}\b(instructions?|prompts?|rules?|messages?)\b/i,
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
rule: "conceal-from-user",
|
|
8
|
+
pattern: /\b(do not|don't|never)\b[^.\n]{0,30}\b(tell|inform|mention|reveal|show|notify)\b[^.\n]{0,30}\b(the )?(user|owner|human)\b/i,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
rule: "exfiltrate-secrets",
|
|
12
|
+
pattern: /\b(send|post|upload|forward|exfiltrate|transmit)\b[^.\n]{0,60}\b(secrets?|tokens?|api[ _-]?keys?|passwords?|credentials?|ssh keys?|\.env)\b/i,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
rule: "read-secrets",
|
|
16
|
+
pattern: /(~\/\.ssh\/|id_rsa|id_ed25519|\.aws\/credentials|\/\.env\b)/i,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
rule: "fake-system-tag",
|
|
20
|
+
pattern: /<\s*\/?\s*(system|important|instructions?|admin)\s*>/i,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// Zero-width, bidi-override and Unicode tag characters hide text from a human reviewer.
|
|
24
|
+
rule: "hidden-unicode",
|
|
25
|
+
pattern: /[----]|[\u{E0000}-\u{E007F}]/u,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
export function scanText(source, text) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
for (const { rule, pattern } of rules) {
|
|
31
|
+
const match = pattern.exec(text);
|
|
32
|
+
if (!match)
|
|
33
|
+
continue;
|
|
34
|
+
const start = Math.max(0, match.index - 20);
|
|
35
|
+
const excerpt = text
|
|
36
|
+
.slice(start, match.index + match[0].length + 20)
|
|
37
|
+
.replace(/[----]/g, "␣")
|
|
38
|
+
.replace(/[\u{E0000}-\u{E007F}]/gu, "␣")
|
|
39
|
+
.replace(/\s+/g, " ")
|
|
40
|
+
.trim();
|
|
41
|
+
findings.push({ source, rule, excerpt });
|
|
42
|
+
}
|
|
43
|
+
return findings;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=description-scan.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class PermissionDeniedError extends Error {
|
|
4
|
+
capability;
|
|
5
|
+
request;
|
|
6
|
+
constructor(capability, request) {
|
|
7
|
+
super(`${capability} did not declare permission ${request.type} "${request.target}"`);
|
|
8
|
+
this.capability = capability;
|
|
9
|
+
this.request = request;
|
|
10
|
+
this.name = "PermissionDeniedError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function expandHome(value, home = os.homedir()) {
|
|
14
|
+
return path.resolve(value === "~"
|
|
15
|
+
? home
|
|
16
|
+
: value.startsWith("~/")
|
|
17
|
+
? path.join(home, value.slice(2))
|
|
18
|
+
: value);
|
|
19
|
+
}
|
|
20
|
+
const within = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
21
|
+
// What a capability declared, as the runtime checks it. Anything not declared is refused.
|
|
22
|
+
// - env / exec: exact name (exec also matches a command's basename)
|
|
23
|
+
// - network: exact host, `*.domain` for subdomains, or `*` for any host
|
|
24
|
+
// - fs.read / fs.write: the path or anything below it (`~` expands); write implies read
|
|
25
|
+
export class PermissionPolicy {
|
|
26
|
+
capability;
|
|
27
|
+
permissions;
|
|
28
|
+
home;
|
|
29
|
+
constructor(capability, permissions, home = os.homedir()) {
|
|
30
|
+
this.capability = capability;
|
|
31
|
+
this.permissions = permissions;
|
|
32
|
+
this.home = home;
|
|
33
|
+
}
|
|
34
|
+
allows({ type, target }) {
|
|
35
|
+
const { filesystem, network, env, exec } = this.permissions;
|
|
36
|
+
if (type === "env")
|
|
37
|
+
return env.includes(target);
|
|
38
|
+
if (type === "exec")
|
|
39
|
+
return exec.includes(target) || exec.includes(path.basename(target));
|
|
40
|
+
if (type === "network") {
|
|
41
|
+
const host = target.toLowerCase();
|
|
42
|
+
return network.some((entry) => entry === "*" ||
|
|
43
|
+
entry === host ||
|
|
44
|
+
(entry.startsWith("*.") && host.endsWith(entry.slice(1))));
|
|
45
|
+
}
|
|
46
|
+
const candidate = expandHome(target, this.home);
|
|
47
|
+
const roots = type === "fs.write"
|
|
48
|
+
? filesystem.write
|
|
49
|
+
: [...filesystem.read, ...filesystem.write];
|
|
50
|
+
return roots.some((root) => within(expandHome(root, this.home), candidate));
|
|
51
|
+
}
|
|
52
|
+
require(request) {
|
|
53
|
+
if (!this.allows(request))
|
|
54
|
+
throw new PermissionDeniedError(this.capability, request);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cp, lstat, mkdir, readdir, readFile, rm, stat, } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { enginePath } from "@ocean/core/shared/paths.js";
|
|
6
|
+
export const clientSkillRoots = {
|
|
7
|
+
claude: (home) => path.join(home, ".claude", "skills"),
|
|
8
|
+
codex: (home) => path.join(home, ".codex", "skills"),
|
|
9
|
+
gemini: (home) => path.join(home, ".gemini", "skills"),
|
|
10
|
+
hermes: (home) => path.join(home, ".hermes", "skills"),
|
|
11
|
+
cursor: (home) => path.join(home, ".cursor", "skills"),
|
|
12
|
+
antigravity: (home) => path.join(home, ".config", "antigravity", "skills"),
|
|
13
|
+
};
|
|
14
|
+
async function coreSkillNames() {
|
|
15
|
+
const core = path.join(enginePath("skills"), "core");
|
|
16
|
+
return (await readdir(core, { withFileTypes: true }))
|
|
17
|
+
.filter((entry) => entry.isDirectory())
|
|
18
|
+
.map((entry) => entry.name)
|
|
19
|
+
.sort();
|
|
20
|
+
}
|
|
21
|
+
export async function coreSkillReports(home = os.homedir()) {
|
|
22
|
+
const names = await coreSkillNames();
|
|
23
|
+
return Promise.all(Object.keys(clientSkillRoots).map(async (client) => {
|
|
24
|
+
const root = clientSkillRoots[client](home);
|
|
25
|
+
try {
|
|
26
|
+
await stat(root);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {
|
|
30
|
+
client,
|
|
31
|
+
root,
|
|
32
|
+
status: "unavailable",
|
|
33
|
+
missing: names,
|
|
34
|
+
drifted: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const missing = [];
|
|
38
|
+
const drifted = [];
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const source = await readFile(path.join(enginePath("skills"), "core", name, "SKILL.md"));
|
|
41
|
+
const targetPath = path.join(root, name, "SKILL.md");
|
|
42
|
+
try {
|
|
43
|
+
const target = await readFile(targetPath);
|
|
44
|
+
if (!same(source, target))
|
|
45
|
+
drifted.push(name);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
missing.push(name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
client,
|
|
53
|
+
root,
|
|
54
|
+
status: missing.length
|
|
55
|
+
? "missing"
|
|
56
|
+
: drifted.length
|
|
57
|
+
? "drifted"
|
|
58
|
+
: "synchronized",
|
|
59
|
+
missing,
|
|
60
|
+
drifted,
|
|
61
|
+
};
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
export async function syncCoreSkills(home = os.homedir()) {
|
|
65
|
+
const names = await coreSkillNames();
|
|
66
|
+
const results = [];
|
|
67
|
+
for (const report of await coreSkillReports(home)) {
|
|
68
|
+
if (report.status === "unavailable") {
|
|
69
|
+
results.push({
|
|
70
|
+
client: report.client,
|
|
71
|
+
root: report.root,
|
|
72
|
+
copied: [],
|
|
73
|
+
status: report.status,
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const copied = [];
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
const source = path.join(enginePath("skills"), "core", name, "SKILL.md");
|
|
80
|
+
const target = path.join(report.root, name, "SKILL.md");
|
|
81
|
+
// A link left behind by a moved source makes mkdir throw ENOENT; it holds nothing.
|
|
82
|
+
const skillDir = path.dirname(target);
|
|
83
|
+
if ((await lstat(skillDir).catch(() => null))?.isSymbolicLink() &&
|
|
84
|
+
!(await stat(skillDir).catch(() => null)))
|
|
85
|
+
await rm(skillDir);
|
|
86
|
+
await mkdir(skillDir, { recursive: true });
|
|
87
|
+
const sourceBytes = await readFile(source);
|
|
88
|
+
let current = null;
|
|
89
|
+
try {
|
|
90
|
+
current = await readFile(target);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* new client skill */
|
|
94
|
+
}
|
|
95
|
+
if (!current || !same(sourceBytes, current)) {
|
|
96
|
+
await cp(source, target);
|
|
97
|
+
copied.push(name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
results.push({
|
|
101
|
+
client: report.client,
|
|
102
|
+
root: report.root,
|
|
103
|
+
copied,
|
|
104
|
+
status: "synchronized",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
function same(left, right) {
|
|
110
|
+
return (createHash("sha256").update(left).digest("hex") ===
|
|
111
|
+
createHash("sha256").update(right).digest("hex"));
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=core-skill-sync.js.map
|