@bevel-software/platform-core-backend 0.28.1 → 0.30.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/agent-guide/access-control.md +15 -0
- package/agent-guide/skills.md +8 -0
- package/dist/__tests__/test-repositories.d.ts +3 -0
- package/dist/__tests__/test-repositories.d.ts.map +1 -0
- package/dist/__tests__/test-repositories.js +18 -0
- package/dist/__tests__/test-repositories.js.map +1 -0
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +4 -2
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/compose-embed.d.ts +35 -0
- package/dist/core/compose-embed.d.ts.map +1 -0
- package/dist/core/compose-embed.js +14 -0
- package/dist/core/compose-embed.js.map +1 -0
- package/dist/core/core-ports.d.ts +10 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +8 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +4 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +57 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +19 -6
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +17 -6
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +20 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +68 -15
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +282 -58
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +20 -6
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
- package/dist/modules/auth/index.d.ts +15 -0
- package/dist/modules/auth/index.d.ts.map +1 -0
- package/dist/modules/auth/index.js +20 -0
- package/dist/modules/auth/index.js.map +1 -0
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +7 -1
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/embed/embed.interface.d.ts +22 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.d.ts +28 -14
- package/dist/modules/embed/embed.service.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.js +38 -17
- package/dist/modules/embed/embed.service.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +10 -2
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/git-dir.d.ts +21 -0
- package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-dir.js +58 -0
- package/dist/modules/kb-fs/git-dir.js.map +1 -0
- package/dist/modules/kb-fs/index.d.ts +9 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -0
- package/dist/modules/kb-fs/index.js +3 -0
- package/dist/modules/kb-fs/index.js.map +1 -0
- package/dist/modules/kb-fs/ref-files.d.ts +63 -0
- package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
- package/dist/modules/kb-fs/ref-files.js +249 -0
- package/dist/modules/kb-fs/ref-files.js.map +1 -0
- package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-jobs.service.js +43 -8
- package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
- package/dist/modules/plugins/teams.routes.d.ts +4 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
- package/dist/modules/plugins/teams.routes.js +9 -1
- package/dist/modules/plugins/teams.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +8 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +115 -11
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
- package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
- package/dist/modules/tool-helpers/default-branch-write.js +63 -0
- package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
- package/dist/modules/tool-helpers/index.d.ts +1 -0
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +1 -0
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts +2 -2
- package/dist/modules/workflow/file-lock.service.d.ts +27 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +70 -18
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +42 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +201 -24
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/index.d.ts +8 -0
- package/dist/modules/workflow/index.d.ts.map +1 -0
- package/dist/modules/workflow/index.js +8 -0
- package/dist/modules/workflow/index.js.map +1 -0
- package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +4 -24
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +20 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +5 -3
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
- package/dist/modules/workspace/agent-access.gate.js +2 -2
- package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
- package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
- package/dist/modules/workspace/bounded-matcher.js +261 -0
- package/dist/modules/workspace/bounded-matcher.js.map +1 -0
- package/dist/modules/workspace/fetch-instant.d.ts +9 -0
- package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
- package/dist/modules/workspace/fetch-instant.js +13 -0
- package/dist/modules/workspace/fetch-instant.js.map +1 -0
- package/dist/modules/workspace/index.d.ts +17 -0
- package/dist/modules/workspace/index.d.ts.map +1 -0
- package/dist/modules/workspace/index.js +16 -0
- package/dist/modules/workspace/index.js.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.js +19 -0
- package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
- package/dist/modules/workspace/kb-repository.d.ts +236 -0
- package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.js +811 -0
- package/dist/modules/workspace/kb-repository.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +0 -5
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
- package/dist/modules/workspace/set-aside-clone.js +139 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +68 -32
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +240 -161
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -4
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +268 -356
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/workspace-id.d.ts +17 -0
- package/dist/shared/workspace-id.d.ts.map +1 -1
- package/dist/shared/workspace-id.js +24 -0
- package/dist/shared/workspace-id.js.map +1 -1
- package/dist/test-setup.js +4 -0
- package/dist/test-setup.js.map +1 -1
- package/mcp-app/page.html +142 -8
- package/package.json +3 -3
- package/src/__tests__/test-repositories.ts +22 -0
- package/src/__tests__/workspace-service.ts +69 -0
- package/src/core/__tests__/compose-embed.test.ts +65 -0
- package/src/core/__tests__/lifecycle.test.ts +45 -26
- package/src/core/catalog-revision.ts +4 -2
- package/src/core/compose-embed.ts +50 -0
- package/src/core/core-ports.ts +10 -0
- package/src/core/create-core-server.ts +8 -2
- package/src/core/create-core-services.ts +63 -24
- package/src/core/lifecycle.ts +34 -12
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +443 -25
- package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/access-control.interface.ts +22 -0
- package/src/modules/access/access-control.service.ts +302 -58
- package/src/modules/access/access.routes.ts +22 -6
- package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
- package/src/modules/auth/index.ts +29 -0
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
- package/src/modules/diff/diff.service.ts +6 -4
- package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
- package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
- package/src/modules/embed/embed.interface.ts +24 -0
- package/src/modules/embed/embed.service.ts +44 -19
- package/src/modules/embed/embed.tools.ts +10 -2
- package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
- package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
- package/src/modules/kb-fs/git-dir.ts +53 -0
- package/src/modules/kb-fs/index.ts +8 -0
- package/src/modules/kb-fs/ref-files.ts +279 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
- package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
- package/src/modules/plugins/join-request-jobs.service.ts +45 -8
- package/src/modules/plugins/teams.routes.ts +14 -1
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
- package/src/modules/skills/skills.contract.ts +4 -0
- package/src/modules/skills/skills.tools.ts +120 -10
- package/src/modules/tool-auth/internal-token.service.ts +1 -1
- package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
- package/src/modules/tool-helpers/default-branch-write.ts +90 -0
- package/src/modules/tool-helpers/index.ts +5 -0
- package/src/modules/tool-helpers/tool-handler.ts +2 -2
- package/src/modules/tool-helpers/tool.contract.ts +2 -2
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +42 -3
- package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
- package/src/modules/workflow/file-lock.service.ts +79 -18
- package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
- package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
- package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
- package/src/modules/workflow/git/git.service.ts +207 -22
- package/src/modules/workflow/index.ts +7 -0
- package/src/modules/workflow/pending-commits.service.ts +4 -23
- package/src/modules/workflow/workflow-hooks.ts +1 -1
- package/src/modules/workflow/workflow.routes.ts +24 -1
- package/src/modules/workflow/workflow.service.ts +3 -2
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
- package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
- package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +174 -309
- package/src/modules/workspace/agent-access.gate.ts +2 -2
- package/src/modules/workspace/bounded-matcher.ts +328 -0
- package/src/modules/workspace/fetch-instant.ts +13 -0
- package/src/modules/workspace/index.ts +26 -0
- package/src/modules/workspace/kb-repository.contract.ts +124 -0
- package/src/modules/workspace/kb-repository.ts +844 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
- package/src/modules/workspace/routine-write-policy.ts +0 -15
- package/src/modules/workspace/set-aside-clone.ts +140 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
- package/src/modules/workspace/workspace.service.ts +247 -169
- package/src/modules/workspace/workspace.tools.ts +299 -385
- package/src/shared/workspace-id.ts +24 -0
- package/src/test-setup.ts +5 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry other modules reach auth through: its admission contract, and
|
|
3
|
+
* the two things a module checking a credential or refusing a switched-off
|
|
4
|
+
* account needs to agree with auth on. The composition root wires the rest.
|
|
5
|
+
*
|
|
6
|
+
* The embed reaches auth here. Modules written before this entry existed
|
|
7
|
+
* still import auth's files directly (`auth.service.ts`, the middleware's
|
|
8
|
+
* request augmentation, `account-admission.ts`); each moves here as it is
|
|
9
|
+
* touched, and what it needs is added here then — this file holds what is
|
|
10
|
+
* offered, not everything auth has.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
ACCOUNT_DEACTIVATED_MESSAGE,
|
|
14
|
+
AccountAdmissionRefusedError,
|
|
15
|
+
AccountChangeRefusedError,
|
|
16
|
+
AccountDeactivatedError,
|
|
17
|
+
AuthBackendError,
|
|
18
|
+
admitEveryone,
|
|
19
|
+
type AccountAdmissionVerdict,
|
|
20
|
+
type AccountProvisionReason,
|
|
21
|
+
type IAccountAdmission,
|
|
22
|
+
} from './account-admission.js';
|
|
23
|
+
// A constant-time comparison for a secret arriving on a request, so a
|
|
24
|
+
// shared-secret check elsewhere cannot leak the secret's length or prefix by
|
|
25
|
+
// timing — one implementation, auth's.
|
|
26
|
+
export { timingSafeStringEqual } from './password-hash.js';
|
|
27
|
+
// The Express `Request` augmentation (`req.userId`, …): a module whose routes
|
|
28
|
+
// read it imports this entry, never the middleware file.
|
|
29
|
+
import './auth.middleware.js';
|
|
@@ -8,6 +8,7 @@ import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
|
8
8
|
import { WorkspaceMutex } from '../../kb-fs/mutex.js';
|
|
9
9
|
import { DiffService } from '../diff.service.js';
|
|
10
10
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
11
|
+
import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Atomic-seed regression suite.
|
|
@@ -53,7 +54,7 @@ async function setup(): Promise<Fixture> {
|
|
|
53
54
|
const workspaceDir = path.join(workspacesRoot, workspaceId);
|
|
54
55
|
const repoDir = path.join(workspaceDir, 'knowledge-base');
|
|
55
56
|
await fs.mkdir(path.join(repoDir, '.git'), { recursive: true });
|
|
56
|
-
const workspaceService =
|
|
57
|
+
const workspaceService = newWorkspaceService(
|
|
57
58
|
workspacesRoot,
|
|
58
59
|
'https://github.com/Bevel-Software/knowledge-base.git',
|
|
59
60
|
testKbContext(),
|
|
@@ -8,6 +8,7 @@ import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
|
8
8
|
import { WorkspaceMutex } from '../../kb-fs/mutex.js';
|
|
9
9
|
import { DiffService } from '../diff.service.js';
|
|
10
10
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
11
|
+
import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
|
|
11
12
|
|
|
12
13
|
async function mkTmpRoot(prefix: string): Promise<string> {
|
|
13
14
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
@@ -39,7 +40,7 @@ async function setup(): Promise<Fixture> {
|
|
|
39
40
|
const repoDir = path.join(workspaceDir, 'knowledge-base');
|
|
40
41
|
// Inner `.git` dir is the marker `resolveWorkspaceDir` looks for.
|
|
41
42
|
await fs.mkdir(path.join(repoDir, '.git'), { recursive: true });
|
|
42
|
-
const workspaceService =
|
|
43
|
+
const workspaceService = newWorkspaceService(
|
|
43
44
|
workspacesRoot,
|
|
44
45
|
'https://github.com/Bevel-Software/knowledge-base.git',
|
|
45
46
|
testKbContext(),
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
8
|
+
import { WorkspaceMutex } from '../../kb-fs/mutex.js';
|
|
9
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
10
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
11
|
+
import { DiffService } from '../diff.service.js';
|
|
12
|
+
import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The review session names the branch the workspace is on, read from HEAD
|
|
16
|
+
* without spawning git. In a worktree `.git` is a FILE naming the worktree's
|
|
17
|
+
* folder in the repository, where its HEAD lives.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
async function git(cwd: string, args: string[]): Promise<void> {
|
|
23
|
+
await execFileAsync('git', args, {
|
|
24
|
+
cwd,
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
|
|
28
|
+
GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let root: string;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-diff-worktree-'));
|
|
37
|
+
});
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('DiffService on a worktree', () => {
|
|
43
|
+
it("names the worktree's own branch in the review session", async () => {
|
|
44
|
+
const upstream = path.join(root, 'upstream.git');
|
|
45
|
+
await git(root, ['init', '--bare', '-b', 'main', upstream]);
|
|
46
|
+
const seed = path.join(root, '.seed');
|
|
47
|
+
await fs.mkdir(seed);
|
|
48
|
+
await git(seed, ['init', '-b', 'alice/draft']);
|
|
49
|
+
await fs.writeFile(path.join(seed, 'page.md'), 'before\n');
|
|
50
|
+
await git(seed, ['add', '-A']);
|
|
51
|
+
await git(seed, ['commit', '-m', 'init']);
|
|
52
|
+
await git(seed, ['push', '-q', upstream, 'alice/draft']);
|
|
53
|
+
|
|
54
|
+
const workspacesRoot = path.join(root, 'workspaces');
|
|
55
|
+
const workspaces = newWorkspaceService(workspacesRoot, upstream, testKbContext(), new NodeFs());
|
|
56
|
+
const diff = new DiffService(
|
|
57
|
+
workspaces,
|
|
58
|
+
new WorkspaceMutex(),
|
|
59
|
+
workspacesRoot,
|
|
60
|
+
path.join(root, 'backups'),
|
|
61
|
+
'knowledge-base',
|
|
62
|
+
new NodeFs(),
|
|
63
|
+
);
|
|
64
|
+
workspaces.setDiffService(diff);
|
|
65
|
+
const workspaceId = workspaceIdForBranch('alice/draft');
|
|
66
|
+
const info = await workspaces.getOrCreateForBranch('alice/draft');
|
|
67
|
+
expect((await fs.stat(path.join(info.absolutePath, 'knowledge-base', '.git'))).isFile()).toBe(true);
|
|
68
|
+
|
|
69
|
+
// The first look seeds the ledger; a change made after it is pending.
|
|
70
|
+
expect(await diff.currentSession(workspaceId)).toBeNull();
|
|
71
|
+
await fs.writeFile(path.join(info.absolutePath, 'knowledge-base', 'page.md'), 'after\n');
|
|
72
|
+
|
|
73
|
+
const session = await diff.currentSession(workspaceId);
|
|
74
|
+
expect(session?.branchName).toBe('alice/draft');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -4,6 +4,7 @@ import type { FileDiffPayload, PendingChange, ReviewSession, ChangeKind } from '
|
|
|
4
4
|
import type { IDiffService } from './diff.interface.js';
|
|
5
5
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
6
6
|
import type { WorkspaceMutex } from '../kb-fs/mutex.js';
|
|
7
|
+
import { gitDirOf } from '../kb-fs/index.js';
|
|
7
8
|
import { isAbsence, type ITreeWalker, type TreeWalkOptions } from '../../shared/fs.contract.js';
|
|
8
9
|
import { isDiffable } from './diff.config.js';
|
|
9
10
|
import { assertWithinDirectory } from '../../shared/path-containment.js';
|
|
@@ -385,10 +386,11 @@ export class DiffService implements IDiffService {
|
|
|
385
386
|
private async tryReadCurrentBranch(workspaceId: string): Promise<string> {
|
|
386
387
|
const workspaceDir = await this.workspaceService.getWorkspacePath(workspaceId);
|
|
387
388
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
);
|
|
389
|
+
// A worktree's `.git` is a FILE naming its folder in the repository,
|
|
390
|
+
// where its HEAD lives; a full clone's is the folder itself.
|
|
391
|
+
const gitDir = await gitDirOf(path.join(workspaceDir, this.kbDirName));
|
|
392
|
+
if (gitDir === null) return '';
|
|
393
|
+
const head = await fs.readFile(path.join(gitDir, 'HEAD'), 'utf-8');
|
|
392
394
|
const m = head.match(/^ref:\s+refs\/heads\/(.+?)\s*$/);
|
|
393
395
|
return m ? m[1] : '';
|
|
394
396
|
} catch {
|
|
@@ -168,15 +168,51 @@ describe('EmbedService: minting', () => {
|
|
|
168
168
|
/**
|
|
169
169
|
* The token rides in the tool result and so in the chat transcript;
|
|
170
170
|
* whoever holds it acts on that one file as its user until it expires.
|
|
171
|
-
*
|
|
171
|
+
* Five minutes (Razvan, 2026-10-09): the chat view renews it through its
|
|
172
|
+
* host when a call is refused, so a short life costs the reader nothing.
|
|
172
173
|
*/
|
|
173
|
-
it('mints
|
|
174
|
+
it('mints an open_page token that lives five minutes', async () => {
|
|
174
175
|
const { service } = build();
|
|
175
176
|
const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
|
|
176
177
|
const { iat, exp } = claimsOf(token) as { iat: number; exp: number };
|
|
178
|
+
expect(exp - iat).toBe(5 * 60);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/** The connector frames `/embed` and has no host to renew through: its hour stands. */
|
|
182
|
+
it('mints a connector token that keeps its one-hour lifetime', async () => {
|
|
183
|
+
const { service } = build();
|
|
184
|
+
const { token } = await service.mintToken({ accountId: 'acc-1', email: USER.email, reference: REPO });
|
|
185
|
+
const { iat, exp } = claimsOf(token) as { iat: number; exp: number };
|
|
177
186
|
expect(exp - iat).toBe(60 * 60);
|
|
178
187
|
});
|
|
179
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Both kinds are read by the same verification: each loads while it lives
|
|
191
|
+
* and is refused as a token once it has run out — the user's after five
|
|
192
|
+
* minutes, the connector's only after the hour.
|
|
193
|
+
*/
|
|
194
|
+
it('reads both kinds while they live and refuses each once its own lifetime is over', async () => {
|
|
195
|
+
vi.useFakeTimers({ toFake: ['Date'] });
|
|
196
|
+
try {
|
|
197
|
+
vi.setSystemTime(new Date('2026-10-09T10:00:00Z'));
|
|
198
|
+
const { service } = build();
|
|
199
|
+
const user = await service.mintForUser({ userId: USER.id, reference: REPO });
|
|
200
|
+
const connector = await service.mintToken({ accountId: 'acc-1', email: USER.email, reference: REPO });
|
|
201
|
+
|
|
202
|
+
vi.setSystemTime(new Date('2026-10-09T10:04:50Z'));
|
|
203
|
+
await expect(service.loadFile(user.token)).resolves.toMatchObject({ repoRelative: REPO });
|
|
204
|
+
|
|
205
|
+
vi.setSystemTime(new Date('2026-10-09T10:05:01Z'));
|
|
206
|
+
await expect(service.loadFile(user.token)).rejects.toThrow(EmbedTokenError);
|
|
207
|
+
await expect(service.loadFile(connector.token)).resolves.toMatchObject({ repoRelative: REPO });
|
|
208
|
+
|
|
209
|
+
vi.setSystemTime(new Date('2026-10-09T11:00:01Z'));
|
|
210
|
+
await expect(service.loadFile(connector.token)).rejects.toThrow(EmbedTokenError);
|
|
211
|
+
} finally {
|
|
212
|
+
vi.useRealTimers();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
180
216
|
it('mints for an outside account — the connector path, unchanged', async () => {
|
|
181
217
|
const { service } = build();
|
|
182
218
|
const { token } = await service.mintToken({
|
|
@@ -189,7 +225,9 @@ describe('EmbedService: minting', () => {
|
|
|
189
225
|
|
|
190
226
|
it('keeps the token PSEUDONYMOUS — no address, no name', async () => {
|
|
191
227
|
const { service } = build();
|
|
192
|
-
const { token } = await service.mintToken({ accountId: 'acc-1', email: USER.email, reference: REPO });
|
|
228
|
+
const { token, embedUrl } = await service.mintToken({ accountId: 'acc-1', email: USER.email, reference: REPO });
|
|
229
|
+
// The connector's host sizes its frame to the content: its view is told so.
|
|
230
|
+
expect(embedUrl).toBe(`https://hexis.example/embed?token=${encodeURIComponent(token)}&sizing=content`);
|
|
193
231
|
expect(JSON.stringify(claimsOf(token))).not.toContain(USER.email);
|
|
194
232
|
expect(JSON.stringify(claimsOf(token))).not.toContain(USER.name);
|
|
195
233
|
});
|
|
@@ -711,6 +749,41 @@ describe('EmbedService: a switched-off account', () => {
|
|
|
711
749
|
});
|
|
712
750
|
});
|
|
713
751
|
|
|
752
|
+
/**
|
|
753
|
+
* A distribution's own token-authed route beside the embed (a graph for a
|
|
754
|
+
* dashboard) asks who is looking before it serves anything, and must judge
|
|
755
|
+
* the viewer exactly as the embed does.
|
|
756
|
+
*/
|
|
757
|
+
describe('EmbedService: who is looking through a token', () => {
|
|
758
|
+
it('answers the linked user, their verdicts on the file, and the file', async () => {
|
|
759
|
+
const { service } = build({ canWrite: false });
|
|
760
|
+
const { token } = await service.mintForUser({ userId: USER.id, reference: `${REPO}#intro` });
|
|
761
|
+
expect(await service.viewerOf(token)).toEqual({
|
|
762
|
+
linked: true,
|
|
763
|
+
user: expect.objectContaining({ id: USER.id, email: USER.email }),
|
|
764
|
+
canRead: true,
|
|
765
|
+
canWrite: false,
|
|
766
|
+
repoRelative: REPO,
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('answers nobody for an unlinked outside account, and refuses a switched-off one as the embed does', async () => {
|
|
771
|
+
const unlinked = build({ linkedUserId: null });
|
|
772
|
+
const outside = await unlinked.service.mintToken({ accountId: 'acc-1', reference: REPO });
|
|
773
|
+
expect(await unlinked.service.viewerOf(outside.token)).toEqual({
|
|
774
|
+
linked: false,
|
|
775
|
+
user: null,
|
|
776
|
+
canRead: false,
|
|
777
|
+
canWrite: false,
|
|
778
|
+
repoRelative: REPO,
|
|
779
|
+
});
|
|
780
|
+
const off = build({ active: false });
|
|
781
|
+
const { token } = await off.service.mintForUser({ userId: USER.id, reference: REPO });
|
|
782
|
+
await expect(off.service.viewerOf(token)).rejects.toThrow(ACCOUNT_DEACTIVATED_MESSAGE);
|
|
783
|
+
await expect(off.service.viewerOf('not-a-token')).rejects.toThrow(EmbedTokenError);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
714
787
|
describe('EmbedService: the roles.yaml write gate', () => {
|
|
715
788
|
const ROLES = `${KB}/roles.yaml`;
|
|
716
789
|
const VALID = 'roles:\n Admin:\n - a@x.eu\n';
|
|
@@ -2,6 +2,7 @@ import type { Server } from 'node:http';
|
|
|
2
2
|
import type { AddressInfo } from 'node:net';
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { checkFor } from '@bevel-software/platform-mcp-core';
|
|
5
6
|
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
6
7
|
import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
|
|
7
8
|
import { ToolError, type ToolContext } from '../../tool-helpers/tool.contract.js';
|
|
@@ -122,6 +123,21 @@ describe('open_page: the listing', () => {
|
|
|
122
123
|
expect(internal).not.toContain(OPEN_PAGE_TOOL);
|
|
123
124
|
});
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The chat view renews its token by having its host call this tool with
|
|
128
|
+
* `{ body: { path, heading } }` (see `mcp-app/page.html`). The listing's
|
|
129
|
+
* own check decides what `/api/mcp` accepts: that shape passes it, and the
|
|
130
|
+
* flat one the view once sent is refused, so no token ever came back.
|
|
131
|
+
*/
|
|
132
|
+
it("accepts the chat view's renewal arguments under body, and refuses them flat", async () => {
|
|
133
|
+
const { registry } = await serve();
|
|
134
|
+
const def = (await registry.listExternal()).find((t) => t.name === OPEN_PAGE_TOOL)!;
|
|
135
|
+
const compiled = checkFor(def.inputs);
|
|
136
|
+
if (!compiled.checkable) throw new Error(`open_page's schema is not checkable: ${compiled.reason}`);
|
|
137
|
+
expect(compiled.check({ body: { path: 'Data/Thing.md', heading: 'what-it-is' } })).toEqual([]);
|
|
138
|
+
expect(compiled.check({ path: 'Data/Thing.md', heading: 'what-it-is' })).not.toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
125
141
|
it('takes a path and an optional heading, and no branch at all', async () => {
|
|
126
142
|
const { registry } = await serve();
|
|
127
143
|
const def = (await registry.listExternal()).find((t) => t.name === OPEN_PAGE_TOOL)!;
|
|
@@ -197,6 +213,19 @@ describe('open_page: the answer', () => {
|
|
|
197
213
|
expect(body.appUrl).toContain('#what-it-is');
|
|
198
214
|
});
|
|
199
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The view renews its token by calling this tool again with the arguments
|
|
218
|
+
* it was opened with, and the result is all it is told — so the heading
|
|
219
|
+
* comes back beside the path, and is absent when none was named.
|
|
220
|
+
*/
|
|
221
|
+
it('echoes the heading beside the path, for the view to call again with', async () => {
|
|
222
|
+
const { call } = await serve();
|
|
223
|
+
const { body } = await call({ path: 'Data/Thing.md', heading: 'what-it-is' });
|
|
224
|
+
expect(body).toMatchObject({ path: 'Data/Thing.md', heading: 'what-it-is' });
|
|
225
|
+
const plain = await call({ path: 'Data/Thing.md' });
|
|
226
|
+
expect(plain.body).not.toHaveProperty('heading');
|
|
227
|
+
});
|
|
228
|
+
|
|
200
229
|
it('accepts a path with or without the knowledge-base prefix, as read_file documents', async () => {
|
|
201
230
|
const { call, mints } = await serve();
|
|
202
231
|
await call({ path: 'Data/Thing.md' });
|
|
@@ -103,6 +103,19 @@ export interface EmbedLinkedAccount {
|
|
|
103
103
|
*/
|
|
104
104
|
export type EmbedNodeIdResolver = (nodeId: string) => Promise<string | null>;
|
|
105
105
|
|
|
106
|
+
/** The viewer behind a token, as {@link IEmbedService.viewerOf} answers it. */
|
|
107
|
+
export interface EmbedViewer {
|
|
108
|
+
/** Whether the token's identity resolved to a user at all. */
|
|
109
|
+
linked: boolean;
|
|
110
|
+
/** The linked user; null when unlinked. */
|
|
111
|
+
user: AuthUser | null;
|
|
112
|
+
/** Read access on the token's file — always false when unlinked. */
|
|
113
|
+
canRead: boolean;
|
|
114
|
+
canWrite: boolean;
|
|
115
|
+
/** The token's file, repository-relative. */
|
|
116
|
+
repoRelative: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
/**
|
|
107
120
|
* What the embed service needs of the workspace: the clone of a branch, and
|
|
108
121
|
* four verbs on one file of it. A port rather than the workspace service
|
|
@@ -165,6 +178,17 @@ export interface IEmbedService {
|
|
|
165
178
|
/** Load the file the token is scoped to, with the viewer's read/write status. */
|
|
166
179
|
loadFile(token: string): Promise<EmbedFileView>;
|
|
167
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Who is looking through `token`, and at what: the resolved viewer (the
|
|
183
|
+
* linked user, or none), their read and write verdicts on the token's
|
|
184
|
+
* file, and the file. The one answer a distribution's own token-authed
|
|
185
|
+
* route beside this surface (a graph for a dashboard, say) asks before it
|
|
186
|
+
* serves anything, so it judges the viewer exactly as the embed does — the
|
|
187
|
+
* same token check, the same account link, the same switched-off refusal.
|
|
188
|
+
* Throws as `loadFile` throws for a token that is invalid or expired.
|
|
189
|
+
*/
|
|
190
|
+
viewerOf(token: string): Promise<EmbedViewer>;
|
|
191
|
+
|
|
168
192
|
/**
|
|
169
193
|
* The raw bytes of the embedded file, or of one asset `path` beside it (an
|
|
170
194
|
* image a markdown page shows — a file no reader edits as text, or an SVG,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHmac } from 'node:crypto';
|
|
2
|
-
import { timingSafeStringEqual } from '../auth/
|
|
3
|
-
import { ACCOUNT_DEACTIVATED_MESSAGE } from '../auth/account-admission.js';
|
|
2
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE, timingSafeStringEqual } from '../auth/index.js';
|
|
4
3
|
import jwt from 'jsonwebtoken';
|
|
5
4
|
import type { AuthUser, IGitService, IWorkflowService } from '@bevel-software/platform-shared';
|
|
6
5
|
import { suggestionsBranchPrefixFor } from '@bevel-software/platform-shared';
|
|
@@ -8,9 +7,9 @@ import { logger } from '../../shared/logging.js';
|
|
|
8
7
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
9
8
|
import type { KbContext } from '../../shared/kb-context.js';
|
|
10
9
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
11
|
-
import { changeRequestLink, changeRequestLinkBase } from '../workflow/
|
|
12
|
-
import type { FileReaderRegistry } from '../workspace/
|
|
13
|
-
import type { WriteValidator } from '../kb-fs/
|
|
10
|
+
import { changeRequestLink, changeRequestLinkBase } from '../workflow/index.js';
|
|
11
|
+
import type { FileReaderRegistry } from '../workspace/index.js';
|
|
12
|
+
import type { WriteValidator } from '../kb-fs/index.js';
|
|
14
13
|
import type {
|
|
15
14
|
EmbedAuthPort,
|
|
16
15
|
EmbedFileView,
|
|
@@ -21,6 +20,7 @@ import type {
|
|
|
21
20
|
EmbedProposalResult,
|
|
22
21
|
EmbedSubject,
|
|
23
22
|
EmbedTokenResult,
|
|
23
|
+
EmbedViewer,
|
|
24
24
|
IEmbedService,
|
|
25
25
|
} from './embed.interface.js';
|
|
26
26
|
import {
|
|
@@ -35,16 +35,24 @@ import type { IAccountLinkService } from './account-link.service.js';
|
|
|
35
35
|
const log = logger('embed');
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* page
|
|
38
|
+
* Lifetime of a token `open_page` mints (`kind: user`). The token rides in
|
|
39
|
+
* the tool's result, so it sits in the chat transcript, and whoever holds it
|
|
40
|
+
* reads and edits that one file as the token's user until it expires. Five
|
|
41
|
+
* minutes (Razvan, 2026-10-09; one hour before, two before that): a token
|
|
42
|
+
* copied out of a transcript stops working almost at once. A view left open
|
|
43
|
+
* longer is not ended by it — the MCP App view asks its host to call
|
|
44
|
+
* `open_page` again when a call is refused for the token, and carries on with
|
|
45
|
+
* the fresh one (see `mcp-app/page.html`).
|
|
46
46
|
*/
|
|
47
|
-
export const
|
|
47
|
+
export const EMBED_USER_TOKEN_TTL_SECONDS = 5 * 60;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Lifetime of a token the Atlassian connector mints through the shared-secret
|
|
51
|
+
* route (`kind: atlassian`). One hour, unchanged: the connector frames
|
|
52
|
+
* `/embed` directly and has no host to renew a token through, and its mint
|
|
53
|
+
* contract does not change (Decision 9 of the parent specification).
|
|
54
|
+
*/
|
|
55
|
+
export const EMBED_CONNECTOR_TOKEN_TTL_SECONDS = 60 * 60;
|
|
48
56
|
|
|
49
57
|
/** What the embed's config needs from the deployment's. */
|
|
50
58
|
export interface EmbedConfig {
|
|
@@ -148,7 +156,11 @@ export class EmbedService implements IEmbedService {
|
|
|
148
156
|
if (!this.authService.isEmailDomainAllowed(email ?? '')) {
|
|
149
157
|
throw new EmbedAccessError('Your email domain is not permitted to view this content');
|
|
150
158
|
}
|
|
151
|
-
|
|
159
|
+
const minted = await this.mint({ kind: 'atlassian', accountId: input.accountId }, input.reference);
|
|
160
|
+
// The connector's host sizes its frame to the content (the issue panel
|
|
161
|
+
// grows with the page), so its view is told to report its height; the
|
|
162
|
+
// MCP path says nothing, and its host's fixed reading pane stays.
|
|
163
|
+
return { ...minted, embedUrl: this.embedUrlFor(minted.token, { sizing: 'content' }) };
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
/**
|
|
@@ -195,13 +207,20 @@ export class EmbedService implements IEmbedService {
|
|
|
195
207
|
repoRelative,
|
|
196
208
|
slug: ref.slug,
|
|
197
209
|
};
|
|
198
|
-
const
|
|
210
|
+
const expiresIn =
|
|
211
|
+
subject.kind === 'user' ? EMBED_USER_TOKEN_TTL_SECONDS : EMBED_CONNECTOR_TOKEN_TTL_SECONDS;
|
|
212
|
+
const token = jwt.sign(claims, this.signingKey, { expiresIn });
|
|
199
213
|
return { token, embedUrl: this.embedUrlFor(token) };
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
|
|
216
|
+
/**
|
|
217
|
+
* The view's address — what a host frames. `sizing: 'content'` tells the
|
|
218
|
+
* view its host sizes the frame to the content and wants the height
|
|
219
|
+
* reported; absent, the host's fixed pane is kept.
|
|
220
|
+
*/
|
|
221
|
+
embedUrlFor(token: string, opts: { sizing?: 'content' } = {}): string {
|
|
222
|
+
const sizing = opts.sizing === 'content' ? '&sizing=content' : '';
|
|
223
|
+
return `${this.config.publicFrontendUrl}/embed?token=${encodeURIComponent(token)}${sizing}`;
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
/** A file's address in the app — what a link opens in a new tab. */
|
|
@@ -556,6 +575,12 @@ export class EmbedService implements IEmbedService {
|
|
|
556
575
|
return { claims, user, wsPath: this.wsPathFor(claims.repoRelative) };
|
|
557
576
|
}
|
|
558
577
|
|
|
578
|
+
async viewerOf(token: string): Promise<EmbedViewer> {
|
|
579
|
+
const claims = this.verifyToken(token);
|
|
580
|
+
const identity = await this.resolveIdentity(claims);
|
|
581
|
+
return { ...identity, repoRelative: claims.repoRelative };
|
|
582
|
+
}
|
|
583
|
+
|
|
559
584
|
/** Resolve the token's identity to a user + read/write verdicts. */
|
|
560
585
|
private async resolveIdentity(claims: EmbedClaims): Promise<ResolvedIdentity> {
|
|
561
586
|
const unresolved: ResolvedIdentity = { linked: false, user: null, canRead: false, canWrite: false };
|
|
@@ -104,6 +104,10 @@ export function registerEmbedTools(
|
|
|
104
104
|
type: 'object',
|
|
105
105
|
properties: {
|
|
106
106
|
path: { type: 'string', description: 'The path that was shown (echoes the input).' },
|
|
107
|
+
heading: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'The heading the view opens at (echoes the input). Absent when none was named.',
|
|
110
|
+
},
|
|
107
111
|
content: { type: 'string', description: "The page's text, exactly as `read_file` answers it." },
|
|
108
112
|
embedUrl: {
|
|
109
113
|
type: 'string',
|
|
@@ -163,12 +167,16 @@ export function registerEmbedTools(
|
|
|
163
167
|
result.kind === 'text' ? result.text : result.kind === 'image' ? result.note : result.message;
|
|
164
168
|
|
|
165
169
|
const appUrl = deps.appUrlFor(repoRelative, heading);
|
|
170
|
+
// The heading goes back beside the path: the view calls this tool again
|
|
171
|
+
// with both when its token runs out (see `mcp-app/page.html`), and a
|
|
172
|
+
// result is all it is told.
|
|
173
|
+
const echoed = heading ? { heading } : {};
|
|
166
174
|
// No view on a plain-http deployment (Decision 8): a host's https
|
|
167
175
|
// sandbox cannot load from it. The text and the app address are still
|
|
168
176
|
// the answer, and the tool says why there is no view rather than
|
|
169
177
|
// handing back an address no host can open.
|
|
170
178
|
if (!deps.canBeReached()) {
|
|
171
|
-
return { path: raw, content, appUrl, branch, note: HTTP_DEPLOYMENT_NOTE };
|
|
179
|
+
return { path: raw, ...echoed, content, appUrl, branch, note: HTTP_DEPLOYMENT_NOTE };
|
|
172
180
|
}
|
|
173
181
|
// Minted for the identity THIS MCP session authenticated — the
|
|
174
182
|
// signed-in user, or the owner of the connection key the agent carries.
|
|
@@ -178,7 +186,7 @@ export function registerEmbedTools(
|
|
|
178
186
|
userId: ctx.user.id,
|
|
179
187
|
reference: heading ? `${repoRelative}#${heading}` : repoRelative,
|
|
180
188
|
});
|
|
181
|
-
return { path: raw, content, embedUrl, appUrl, branch };
|
|
189
|
+
return { path: raw, ...echoed, content, embedUrl, appUrl, branch };
|
|
182
190
|
}),
|
|
183
191
|
);
|
|
184
192
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
import { gitDirOf, readGitfile } from '../git-dir.js';
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
async function git(cwd: string, ...args: string[]): Promise<string> {
|
|
13
|
+
const { stdout } = await execFileAsync('git', ['-C', cwd, ...args], { encoding: 'utf-8' });
|
|
14
|
+
return stdout.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The one resolver of a checkout's git directory, checked against what git
|
|
19
|
+
* itself answers (`rev-parse --absolute-git-dir`) for a clone and a worktree.
|
|
20
|
+
*/
|
|
21
|
+
describe('gitDirOf', () => {
|
|
22
|
+
let root: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'git-dir-')));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('is the .git folder of a clone and the folder a worktree .git file names, as git says', async () => {
|
|
33
|
+
const clone = path.join(root, 'clone');
|
|
34
|
+
await fs.mkdir(clone);
|
|
35
|
+
await git(clone, 'init', '-q');
|
|
36
|
+
await git(clone, '-c', 'user.name=T', '-c', 'user.email=t@x.com', 'commit', '-q', '--allow-empty', '-m', 'c');
|
|
37
|
+
const worktree = path.join(root, 'wt');
|
|
38
|
+
await git(clone, 'worktree', 'add', '-q', '-b', 'side', worktree);
|
|
39
|
+
|
|
40
|
+
for (const dir of [clone, worktree]) {
|
|
41
|
+
const fromGit = path.resolve(await git(dir, 'rev-parse', '--absolute-git-dir'));
|
|
42
|
+
expect(path.resolve((await gitDirOf(dir))!)).toBe(await fs.realpath(fromGit));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is null with no .git, and with a .git file that names no folder', async () => {
|
|
47
|
+
expect(await gitDirOf(root)).toBeNull();
|
|
48
|
+
await fs.writeFile(path.join(root, '.git'), 'not a gitfile\n');
|
|
49
|
+
expect(await gitDirOf(root)).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reads exactly what git reads: `gitdir: ` and a path, nothing before, after or around it', async () => {
|
|
53
|
+
const gitfile = path.join(root, '.git');
|
|
54
|
+
for (const text of [
|
|
55
|
+
'not a gitfile\ngitdir: ../elsewhere/wt\n', // a gitdir line further down
|
|
56
|
+
'gitdir: ../elsewhere/wt\njunk\n', // a second line after the path
|
|
57
|
+
'gitdir:../elsewhere/wt\n', // no space after the colon
|
|
58
|
+
'gitdir: ../elsewhere/wt\n', // a path that starts with a space, which exists nowhere
|
|
59
|
+
'\ngitdir: ../elsewhere/wt\n', // a blank first line
|
|
60
|
+
]) {
|
|
61
|
+
await fs.writeFile(gitfile, text);
|
|
62
|
+
const named = await readGitfile(gitfile);
|
|
63
|
+
// Git refuses each as a gitfile; what this reads of it names no folder
|
|
64
|
+
// git would open.
|
|
65
|
+
await expect(git(root, 'rev-parse', '--absolute-git-dir')).rejects.toThrow();
|
|
66
|
+
expect(named === null || named !== path.resolve(root, '../elsewhere/wt'), JSON.stringify(text)).toBe(true);
|
|
67
|
+
}
|
|
68
|
+
await fs.writeFile(gitfile, 'gitdir: ../elsewhere/wt\r\n');
|
|
69
|
+
expect(await readGitfile(gitfile)).toBe(path.resolve(root, '../elsewhere/wt'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('resolves a relative gitdir against the file’s own folder', async () => {
|
|
73
|
+
await fs.writeFile(path.join(root, '.git'), 'gitdir: ../elsewhere/wt\n');
|
|
74
|
+
expect(await readGitfile(path.join(root, '.git'))).toBe(path.resolve(root, '../elsewhere/wt'));
|
|
75
|
+
expect(await gitDirOf(root)).toBe(path.resolve(root, '../elsewhere/wt'));
|
|
76
|
+
});
|
|
77
|
+
});
|