@dharma-ai-labs/agent-fabric 0.2.126 → 0.2.128

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/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import { openCodexAppServerTransport } from '@dharma-ai-labs/agent-fabric-provid
61
61
  import { createNamedSessionTrust, isNamedSessionOwnerReceipt, renewNamedSessionLifetime } from './namedSessionTrust.js';
62
62
  import { startNamedCodexThread } from './namedCodexThread.js';
63
63
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
64
- const VERSION = '0.2.126';
64
+ const VERSION = '0.2.128';
65
65
  const USAGE = CLI_USAGE;
66
66
  const execFileAsync = promisify(execFile);
67
67
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -1768,9 +1768,15 @@ async function supervisedDemoCycle(registration, signal) {
1768
1768
  if (workspace !== registration.workspace)
1769
1769
  throw new Error('Demo watch checkout is no longer canonical.');
1770
1770
  const root = await gitValue(workspace, ['rev-parse', '--show-toplevel']);
1771
- const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
1772
- if (!root || !remote || normalizeGitRemoteIdentity(remote) !== registration.normalizedRepository) {
1773
- throw Object.assign(new Error('Demo watch repository binding changed.'), { code: 'demo_watch_repository_changed' });
1771
+ if (registration.knowledgeOnly) {
1772
+ if (root)
1773
+ throw Object.assign(new Error('Demo knowledge-only workspace became a Git checkout.'), { code: 'demo_watch_workspace_changed' });
1774
+ }
1775
+ else {
1776
+ const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
1777
+ if (!root || !remote || normalizeGitRemoteIdentity(remote) !== registration.normalizedRepository) {
1778
+ throw Object.assign(new Error('Demo watch repository binding changed.'), { code: 'demo_watch_repository_changed' });
1779
+ }
1774
1780
  }
1775
1781
  const scope = { hqUrl: registration.hqUrl, organizationId: registration.organizationId,
1776
1782
  repositoryId: registration.repositoryId, normalizedRepository: registration.normalizedRepository,
@@ -1786,6 +1792,7 @@ async function supervisedDemoCycle(registration, signal) {
1786
1792
  signal: options?.signal ? AbortSignal.any([options.signal, signal]) : signal });
1787
1793
  } });
1788
1794
  return demoRepositoryPackage({ scope, workspace, provider: registration.provider,
1795
+ knowledgeOnly: registration.knowledgeOnly,
1789
1796
  nativeSkillDirectory: nativeDirectory }, deps);
1790
1797
  });
1791
1798
  }
@@ -2047,6 +2054,16 @@ async function bootstrap(flags) {
2047
2054
  const resuming = flags.has('resume');
2048
2055
  const bootstrapToken = resuming ? null : required(flags, 'grant');
2049
2056
  const selectedRemote = flags.get('repository-url-base64url');
2057
+ const joinedBindingId = typeof flags.get('join-repository-binding-id') === 'string'
2058
+ ? String(flags.get('join-repository-binding-id')) : null;
2059
+ const joinedFingerprint = typeof flags.get('join-source-fingerprint') === 'string'
2060
+ ? String(flags.get('join-source-fingerprint')) : null;
2061
+ if (joinedBindingId && (!UUID_PATTERN.test(joinedBindingId) || !/^sha256:[a-f0-9]{64}$/.test(joinedFingerprint || '')
2062
+ || selectedRemote || flags.has('repository-key'))) {
2063
+ throw new Error('repository_join_selection_invalid: joining requires an exact existing binding and fingerprint, without a source selection.');
2064
+ }
2065
+ if (!joinedBindingId && joinedFingerprint)
2066
+ throw new Error('repository_join_selection_invalid');
2050
2067
  let repositorySelection;
2051
2068
  try {
2052
2069
  if (flags.has('repository-url-base64url') && typeof selectedRemote !== 'string') {
@@ -2055,16 +2072,18 @@ async function bootstrap(flags) {
2055
2072
  if (selectedRemote && flags.has('repository-key')) {
2056
2073
  throw new Error('repository_selection_invalid_url: selected remote cannot be combined with a repository key.');
2057
2074
  }
2058
- repositorySelection = typeof selectedRemote === 'string'
2059
- ? await resolveBootstrapRepositoryWorkspace({
2060
- workspace: String(flags.get('workspace') || '.'),
2061
- selectedRemoteBase64url: selectedRemote,
2062
- organizationId,
2063
- home: dharmaHome(),
2064
- normalizeRemote: normalizeGitRemoteIdentity,
2065
- withLock: (path) => acquirePidLock(path, 30_000, 'repository_checkout_busy: another setup is selecting this repository; retry after it finishes.'),
2066
- })
2067
- : { workspace: await realpath(String(flags.get('workspace') || '.')), selection: 'provided' };
2075
+ repositorySelection = joinedBindingId
2076
+ ? { workspace: resolve(dharmaHome(), 'joined-repositories', joinedBindingId), selection: 'existing_organization_binding' }
2077
+ : typeof selectedRemote === 'string'
2078
+ ? await resolveBootstrapRepositoryWorkspace({
2079
+ workspace: String(flags.get('workspace') || '.'),
2080
+ selectedRemoteBase64url: selectedRemote,
2081
+ organizationId,
2082
+ home: dharmaHome(),
2083
+ normalizeRemote: normalizeGitRemoteIdentity,
2084
+ withLock: (path) => acquirePidLock(path, 30_000, 'repository_checkout_busy: another setup is selecting this repository; retry after it finishes.'),
2085
+ })
2086
+ : { workspace: await realpath(String(flags.get('workspace') || '.')), selection: 'provided' };
2068
2087
  }
2069
2088
  catch (error) {
2070
2089
  const message = error && typeof error === 'object' && 'message' in error && typeof error.message === 'string'
@@ -2079,10 +2098,12 @@ async function bootstrap(flags) {
2079
2098
  if (selectedRemote)
2080
2099
  process.stderr.write(`Agent Fabric repository checkout: ${workspace}\n`);
2081
2100
  const policyRevision = required(flags, 'policy-revision');
2082
- const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
2101
+ const repositoryIdentity = joinedBindingId
2102
+ ? { fingerprint: joinedFingerprint }
2103
+ : await preflightBootstrapWorkspaceIdentity(workspace, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
2083
2104
  const requestedProvider = String(flags.get('provider') || 'auto').trim().toLowerCase();
2084
2105
  const provider = requestedProvider === 'auto'
2085
- ? await detectBootstrapProvider(workspace)
2106
+ ? await detectBootstrapProvider(joinedBindingId ? String(flags.get('workspace') || '.') : workspace)
2086
2107
  : requestedProvider;
2087
2108
  if (!isLocalProviderId(provider)) {
2088
2109
  throw new Error('Bootstrap provider must be auto, codex, claude, agy, or hermes.');
@@ -2121,7 +2142,7 @@ async function bootstrap(flags) {
2121
2142
  name,
2122
2143
  platform: devicePlatform,
2123
2144
  publicKeyEd25519: identity.publicKeyEd25519,
2124
- repositoryFingerprint: repositoryIdentity.fingerprint,
2145
+ repositoryFingerprint: joinedBindingId ? '' : repositoryIdentity.fingerprint,
2125
2146
  onRecipientApprovalRequired: async (approval) => {
2126
2147
  recipientApproval.required = true;
2127
2148
  recipientApproval.browserOpened = flags.has('no-browser')
@@ -2204,7 +2225,9 @@ async function bootstrap(flags) {
2204
2225
  onboardFlags.set('workspace', workspace);
2205
2226
  onboardFlags.set('policy-revision', policyRevision);
2206
2227
  onboardFlags.set('provider', provider);
2207
- const onboarded = await retryBootstrapOnboarding(async () => await onboard(onboardFlags));
2228
+ const onboarded = await retryBootstrapOnboarding(async () => joinedBindingId
2229
+ ? await joinExistingRepository(onboardFlags, joinedBindingId, joinedFingerprint)
2230
+ : await onboard(onboardFlags));
2208
2231
  if (onboarded.ok !== true || onboarded.stage === 'approve_device') {
2209
2232
  return {
2210
2233
  ok: onboarded.ok === true,
@@ -2227,7 +2250,8 @@ async function bootstrap(flags) {
2227
2250
  const launcher = await installStableRepositoryLauncher(workspace);
2228
2251
  const autostart = flags.has('no-relay-daemon')
2229
2252
  ? { state: 'disabled', backend: null }
2230
- : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(async () => {
2253
+ : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`
2254
+ + (joinedBindingId ? ` --join-repository-binding-id ${joinedBindingId} --join-source-fingerprint ${joinedFingerprint}` : ''), () => withRelayStartupMutation(async () => {
2231
2255
  const home = dharmaHome();
2232
2256
  const existing = await relayAutostartStatus({ home });
2233
2257
  if (existing.backend !== null || existing.state !== 'disabled') {
@@ -2285,7 +2309,8 @@ async function bootstrap(flags) {
2285
2309
  throw new Error(`Provider skill did not become ready: ${JSON.stringify(skill)}`);
2286
2310
  const policyPath = resolve(workspace, '.dharma', 'approved-policy.json');
2287
2311
  const firstLearning = onboarded.firstLearningEvidence;
2288
- const firstLearningReady = firstLearning?.state === 'synchronized' || firstLearning?.state === 'denied_disclosure';
2312
+ const firstLearningReady = firstLearning?.state === 'synchronized' || firstLearning?.state === 'denied_disclosure'
2313
+ || joinedBindingId !== null && firstLearning?.state === 'shared_package_inherited';
2289
2314
  const relay = flags.has('no-relay-daemon')
2290
2315
  ? { started: false, ...(await waitForRelayReadiness()) }
2291
2316
  : await startRelayDaemon(policyPath);
@@ -2329,7 +2354,7 @@ async function bootstrap(flags) {
2329
2354
  usage,
2330
2355
  });
2331
2356
  const operatingContract = await loadAgentFabricOnboardingContract();
2332
- const namedSession = provider === 'codex' && process.platform === 'linux' && sharedRepositoryReady
2357
+ const namedSession = !joinedBindingId && provider === 'codex' && process.platform === 'linux' && sharedRepositoryReady
2333
2358
  ? await withOnboardingStage('named_session', String(onboarded.workspaceId), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => namedSessionCommand('start', new Map([
2334
2359
  ['name', String(flags.get('session-name') || `codex-${String(onboarded.workspaceId).slice(0, 8)}`)],
2335
2360
  ['workspace-id', String(onboarded.workspaceId)], ['apply', true],
@@ -2338,13 +2363,23 @@ async function bootstrap(flags) {
2338
2363
  ])))
2339
2364
  : null;
2340
2365
  const repositoryReceipt = onboarded;
2341
- const namedSessionReady = provider !== 'codex' || process.platform !== 'linux'
2366
+ const namedSessionReady = Boolean(joinedBindingId) || provider !== 'codex' || process.platform !== 'linux'
2342
2367
  || Boolean(namedSession?.ok === true && ['running', 'executing'].includes(String(namedSession.state)));
2343
2368
  const role = repositoryReceipt.repositoryRole;
2369
+ const roleReady = Boolean(role);
2370
+ const relayReady = relay.state === 'running';
2371
+ const startupReady = !['win32', 'linux'].includes(process.platform) || autostart.state === 'enabled';
2372
+ const complete = sharedRepositoryReady && namedSessionReady && firstLearningReady
2373
+ && roleReady && relayReady && startupReady;
2344
2374
  return {
2345
- ok: sharedRepositoryReady && namedSessionReady && firstLearningReady,
2346
- stage: sharedRepositoryReady ? namedSessionReady ? firstLearningReady ? 'complete' : 'first_learning_pending' : 'named_session_pending'
2347
- : repositoryReadiness?.outcome === 'blocked' ? 'shared_repository_blocked' : 'shared_repository_pending',
2375
+ ok: complete,
2376
+ stage: !sharedRepositoryReady
2377
+ ? repositoryReadiness?.outcome === 'blocked' ? 'shared_repository_blocked' : 'shared_repository_pending'
2378
+ : !namedSessionReady ? 'named_session_pending'
2379
+ : !firstLearningReady ? 'first_learning_pending'
2380
+ : !roleReady ? 'role_registration_pending'
2381
+ : !relayReady ? 'synchronization_pending'
2382
+ : !startupReady ? 'autostart_pending' : 'complete',
2348
2383
  repositorySelection,
2349
2384
  localStage: 'complete',
2350
2385
  sharedRepositoryReady,
@@ -2379,13 +2414,13 @@ async function bootstrap(flags) {
2379
2414
  },
2380
2415
  workflowReadiness: {
2381
2416
  sharedPackage: sharedRepositoryReady ? 'ready' : 'pending',
2382
- roleRegistration: role ? 'ready' : 'pending',
2417
+ roleRegistration: roleReady ? 'ready' : 'pending',
2383
2418
  firstLearning: firstLearning?.state === 'synchronized'
2384
2419
  ? Number(firstLearning.discovered || 0) > 0 ? 'synchronized' : 'no_eligible_history'
2385
2420
  : String(firstLearning?.state || 'pending'),
2386
2421
  synchronization: relay.state === 'running' ? 'running' : 'pending',
2387
2422
  namedSession: namedSession && namedSessionReady ? 'ready'
2388
- : provider === 'codex' && process.platform === 'linux' ? 'pending' : 'not_supported',
2423
+ : joinedBindingId ? 'not_requested' : provider === 'codex' && process.platform === 'linux' ? 'pending' : 'not_supported',
2389
2424
  },
2390
2425
  };
2391
2426
  }
@@ -3106,6 +3141,9 @@ async function evidencePreview(flags) {
3106
3141
  }
3107
3142
  async function workspaceAdd(flags, positional) {
3108
3143
  const path = await realpath(positional[0] || required(flags, 'path'));
3144
+ if ((await registry()).some(record => record.path === path && record.accessMode === 'knowledge_only')) {
3145
+ throw new Error('repository_join_source_operation_denied');
3146
+ }
3109
3147
  const device = JSON.parse(await readFile(configPath(), 'utf8'));
3110
3148
  const organizationId = String(flags.get('organization-id') || device.organizationId);
3111
3149
  if (organizationId !== device.organizationId)
@@ -3477,7 +3515,7 @@ export function canonicalEndpointRole(value) {
3477
3515
  }
3478
3516
  return { revision: Number(row.revision), profileHash: row.profileHash };
3479
3517
  }
3480
- async function bindRepositoryAgent(fabric, item) {
3518
+ async function bindRepositoryAgent(fabric, item, joinExistingBindingId) {
3481
3519
  const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
3482
3520
  const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
3483
3521
  ? item.repositoryRemoteHash
@@ -3491,6 +3529,7 @@ async function bindRepositoryAgent(fabric, item) {
3491
3529
  defaultSourceRef: item.defaultBranch,
3492
3530
  workspaceId: item.workspaceId,
3493
3531
  legacySourceFingerprint: item.repositoryIdentityVersion === 'normalized-v1' ? null : item.repositoryRemoteHash,
3532
+ ...(joinExistingBindingId ? { joinExistingBindingId } : {}),
3494
3533
  });
3495
3534
  const repositoryAgent = response.repositoryAgent && typeof response.repositoryAgent === 'object'
3496
3535
  ? response.repositoryAgent
@@ -3532,6 +3571,9 @@ async function bindRepositoryAgent(fabric, item) {
3532
3571
  || !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
3533
3572
  throw new Error('Dharma HQ returned an invalid repository-agent binding.');
3534
3573
  }
3574
+ if (joinExistingBindingId && updated.repositoryBindingId !== joinExistingBindingId) {
3575
+ throw new Error('repository_join_binding_mismatch');
3576
+ }
3535
3577
  await saveWorkspaceRecord(updated);
3536
3578
  return updated;
3537
3579
  }
@@ -3863,6 +3905,102 @@ async function registerWorkspaceBeforeRepositoryBind(fabric, item, policyRevisio
3863
3905
  const registered = await bindRepositoryAgent(fabric, canonical);
3864
3906
  return { registered, synchronized };
3865
3907
  }
3908
+ async function joinExistingRepository(flags, bindingId, sourceFingerprint) {
3909
+ const config = await readDeviceConfig();
3910
+ if (!config || config.organizationId !== required(flags, 'organization-id')) {
3911
+ throw new Error('repository_join_enrollment_required');
3912
+ }
3913
+ const workspace = resolve(dharmaHome(), 'joined-repositories', bindingId);
3914
+ await mkdir(workspace, { recursive: true, mode: 0o700 });
3915
+ if (await realpath(workspace) !== workspace)
3916
+ throw new Error('repository_join_workspace_symlink_denied');
3917
+ const markerPath = resolve(workspace, '.dharma', 'join-identity.json');
3918
+ const expectedMarker = { organizationId: config.organizationId, bindingId, sourceFingerprint };
3919
+ const existingMarker = await readFile(markerPath, 'utf8').then(value => JSON.parse(value))
3920
+ .catch(error => { if (error?.code === 'ENOENT')
3921
+ return null; throw error; });
3922
+ if (existingMarker && canonicalize(existingMarker) !== canonicalize(expectedMarker)) {
3923
+ throw new Error('repository_join_workspace_identity_conflict');
3924
+ }
3925
+ if (!existingMarker) {
3926
+ const entries = await readdir(workspace);
3927
+ if (entries.length)
3928
+ throw new Error('repository_join_workspace_not_empty');
3929
+ await mkdir(resolve(workspace, '.dharma'), { recursive: true, mode: 0o700 });
3930
+ await writeJsonAtomic(markerPath, expectedMarker);
3931
+ }
3932
+ if (!await gitValue(workspace, ['rev-parse', '--verify', 'HEAD'])) {
3933
+ const template = resolve(workspace, '.dharma', 'empty-git-template');
3934
+ await mkdir(template, { recursive: true, mode: 0o700 });
3935
+ await execFileAsync('git', ['-c', `init.templateDir=${template}`, 'init', '--initial-branch=main', workspace]);
3936
+ await execFileAsync('git', ['-C', workspace, '-c', 'user.name=Dharma Agent Fabric',
3937
+ '-c', 'user.email=agent-fabric@localhost', '-c', 'commit.gpgsign=false', '-c', `core.hooksPath=${template}`,
3938
+ 'commit', '--allow-empty', '-m', 'Initialize isolated knowledge workspace']);
3939
+ }
3940
+ const workspaceId = workspaceIdForDevice({ organizationId: config.organizationId,
3941
+ deviceId: config.deviceId, path: workspace });
3942
+ let item = selectDeviceWorkspace(await registry(), { organizationId: config.organizationId,
3943
+ deviceId: config.deviceId, path: workspace, repositoryRemoteHash: sourceFingerprint });
3944
+ if (item && item.accessMode !== 'knowledge_only')
3945
+ throw new Error('repository_join_workspace_mode_conflict');
3946
+ if (!item) {
3947
+ item = {
3948
+ workspaceId, organizationId: config.organizationId, name: `joined-${bindingId.slice(0, 8)}`,
3949
+ path: workspace, routeHash: `sha256:${createHash('sha256').update(workspace).digest('hex')}`,
3950
+ repositoryRemoteHash: sourceFingerprint, repositoryIdentityVersion: 'normalized-v1',
3951
+ repositoryAgentId: null, repositoryBindingId: null, repositoryAgentKey: null,
3952
+ controlBranch: null, defaultBranch: null, status: 'active', accessMode: 'knowledge_only',
3953
+ };
3954
+ await saveWorkspaceRecord(item);
3955
+ }
3956
+ const provider = String(flags.get('provider') || 'codex');
3957
+ if (!isLocalProviderId(provider))
3958
+ throw new Error('repository_join_provider_invalid');
3959
+ const fabric = await client();
3960
+ const synchronized = await syncWorkspacePolicy(fabric, item, required(flags, 'policy-revision'), true, [provider]);
3961
+ item = reconcileWorkspaceRegistration(await registry(), item, synchronized);
3962
+ item = await bindRepositoryAgent(fabric, item, bindingId);
3963
+ if (item.repositoryPackage?.state !== 'published' || !item.repositoryAgentId || !item.endpointId) {
3964
+ return { ok: false, stage: 'shared_repository_pending', code: 'repository_join_package_unavailable',
3965
+ workspaceId: item.workspaceId, sharedRepositoryReady: false };
3966
+ }
3967
+ const roleInput = {
3968
+ roleName: String(flags.get('role-name') || 'Knowledge collaborator'),
3969
+ questionCategories: (typeof flags.get('question-categories') === 'string'
3970
+ ? String(flags.get('question-categories')).split(',').map(value => value.trim()).filter(Boolean)
3971
+ : ['repository-knowledge', 'lexicon', 'skills']).sort(),
3972
+ description: String(flags.get('role-description') || 'Uses the signed repository package and answers bounded knowledge questions.'),
3973
+ };
3974
+ const profileHash = sha256(canonicalize(roleInput));
3975
+ const role = item.repositoryRole?.profileHash === profileHash
3976
+ ? { stage: 'repository_role_observed', reused: true }
3977
+ : await registerRepositoryRoleMetadata(fabric, repositoryRoleScope(item), {
3978
+ expectedRevision: item.repositoryRole?.revision || 0, ...roleInput,
3979
+ });
3980
+ if (item.repositoryRole?.profileHash !== profileHash) {
3981
+ const revision = Number(role.role?.revision);
3982
+ if (!Number.isSafeInteger(revision) || revision < 1)
3983
+ throw new Error('repository_join_role_registration_failed');
3984
+ item = { ...item, repositoryRole: { revision, profileHash } };
3985
+ await saveWorkspaceRecord(item);
3986
+ }
3987
+ const providers = await receiptAwareProviderCapabilities(await Promise.all(selectedProviderAdapters([provider]).map(adapter => adapter.capability())));
3988
+ const nativeSkills = await installAvailableNativeAgentFabricBootstraps({ providers,
3989
+ workspace, workspaceId: item.workspaceId, organizationId: config.organizationId, hqUrl: config.hqUrl });
3990
+ const relay = flags.has('no-relay-daemon') ? { started: false, state: 'not_started' }
3991
+ : await startRelayDaemon(resolve(workspace, '.dharma', 'approved-policy.json'));
3992
+ const sharedRepositoryReady = await repositorySharedReady(item);
3993
+ return { ok: true, stage: sharedRepositoryReady ? 'ready' : 'shared_repository_pending',
3994
+ localStage: 'ready', sharedRepositoryReady, organizationId: config.organizationId,
3995
+ workspaceId: item.workspaceId, deviceId: config.deviceId, accessMode: 'knowledge_only',
3996
+ repositoryAgent: { id: item.repositoryAgentId, key: item.repositoryAgentKey,
3997
+ bindingId: item.repositoryBindingId, controlBranch: item.controlBranch },
3998
+ repositoryPackage: item.repositoryPackage, repositoryRole: role, providers,
3999
+ firstLearningEvidence: { state: 'shared_package_inherited', discovered: 0,
4000
+ disclosureReady: false, note: 'No local source or trajectory history was read.' },
4001
+ nativeSkills: nativeSkills.installed, nativeSkillFailures: nativeSkills.failures,
4002
+ workspaceSync: synchronized, relay };
4003
+ }
3866
4004
  async function readDeviceConfig() {
3867
4005
  try {
3868
4006
  return JSON.parse(await readFile(configPath(), 'utf8'));
@@ -4119,6 +4257,9 @@ async function repositorySnapshotCommand(flags, outputs) {
4119
4257
  }
4120
4258
  const registeredWorkspace = (await registry()).find(record => record.path === resolve(workspace)
4121
4259
  && record.organizationId === flags.get('organization-id') && record.workspaceId === flags.get('workspace-id'));
4260
+ if (registeredWorkspace?.accessMode === 'knowledge_only') {
4261
+ throw new Error('repository_join_source_operation_denied');
4262
+ }
4122
4263
  const sourceAuthorization = registeredWorkspace?.repositoryAgentId && registeredWorkspace.repositoryBindingId
4123
4264
  ? await fetchRepositorySourceAuthorization(await client(), {
4124
4265
  organizationId: registeredWorkspace.organizationId,
@@ -4175,7 +4316,6 @@ async function onboard(flags) {
4175
4316
  ? [String(flags.get('providers'))]
4176
4317
  : typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
4177
4318
  const repositoryKey = typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null;
4178
- const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, repositoryKey);
4179
4319
  const registryState = await inspectRegistryRecoveryFile(workspaceRegistryPath());
4180
4320
  if (registryState.kind === 'corrupt_zero') {
4181
4321
  const anchor = await inspectOwnedRelayAutostart({ home: dharmaHome() });
@@ -4185,6 +4325,10 @@ async function onboard(flags) {
4185
4325
  ['workspace', anchor.workspace], ['apply', true],
4186
4326
  ]));
4187
4327
  }
4328
+ if ((await registry()).some(record => record.path === workspace && record.accessMode === 'knowledge_only')) {
4329
+ throw new Error('repository_join_source_operation_denied');
4330
+ }
4331
+ const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, repositoryKey);
4188
4332
  let registered = selectDeviceWorkspace(await registry(), {
4189
4333
  organizationId,
4190
4334
  deviceId: config.deviceId,
@@ -4959,6 +5103,16 @@ async function executeOneTask(fabric, leaseSeconds, exactStudyTask) {
4959
5103
  const workspace = (await registry()).find((item) => item.workspaceId === task.workspaceId);
4960
5104
  if (!workspace)
4961
5105
  throw new Error('Task workspace is not registered on this device.');
5106
+ if (workspace.accessMode === 'knowledge_only' && !(task.taskType === 'external_request'
5107
+ && task.authority.readPaths.length === 1 && task.authority.readPaths[0] === '.'
5108
+ && task.authority.writePaths.length === 0 && task.authority.commands.length === 0
5109
+ && task.authority.network === 'deny' && task.acceptance.commands.length === 0
5110
+ && task.acceptance.requiredArtifacts.length === 0)) {
5111
+ await fabric.postTaskEvent(task.taskId, 'failed', {
5112
+ phase: 'preflight', code: 'knowledge_only_task_denied',
5113
+ });
5114
+ return { ok: false, taskId: task.taskId, code: 'knowledge_only_task_denied' };
5115
+ }
4962
5116
  const taskPolicy = await refreshVerifiedWorkspacePolicyForTransmission(resolve(workspace.path, '.dharma', 'approved-policy.json'), workspace.workspaceId, fabric);
4963
5117
  assertTaskWorkspacePolicy({ task, workspace, policy: taskPolicy });
4964
5118
  const config = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -6294,7 +6448,8 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6294
6448
  || (canonicalWorkspace.repositoryPackage?.state === 'published'
6295
6449
  && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6296
6450
  ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6297
- if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6451
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && canonicalWorkspace.repositoryPackage
6452
+ && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6298
6453
  try {
6299
6454
  const recovered = await recoverPublishedLocalSourceBaseline({
6300
6455
  workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
@@ -6509,7 +6664,8 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6509
6664
  }
6510
6665
  nextPolicyRefreshAt = Date.now() + 60_000;
6511
6666
  }
6512
- if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6667
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && !sourceScanFlight
6668
+ && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6513
6669
  try {
6514
6670
  const current = await pollRepositoryCandidate({ transport: fabric,
6515
6671
  outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
@@ -6538,7 +6694,7 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6538
6694
  }
6539
6695
  }
6540
6696
  let evidenceRequestId;
6541
- if (evidencePolicyFresh && !stopping) {
6697
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && evidencePolicyFresh && !stopping) {
6542
6698
  const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6543
6699
  const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6544
6700
  const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
@@ -6610,7 +6766,8 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6610
6766
  }
6611
6767
  nextSkillActivationAt = performance.now() + 60_000;
6612
6768
  }
6613
- if (!stopping && !sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6769
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && !stopping && !sourceScanFlight
6770
+ && performance.now() >= nextRepositorySourceScanAt) {
6614
6771
  const sourceWorkspace = canonicalWorkspace;
6615
6772
  const sourceBaselineHash = publishedLocalSnapshotHash;
6616
6773
  sourceScanFlight = (async () => {
@@ -6693,20 +6850,30 @@ export async function run(argv) {
6693
6850
  const organizationId = required(flags, 'organization-id');
6694
6851
  const repositoryId = required(flags, 'repository-id');
6695
6852
  const normalizedRepository = required(flags, 'normalized-repository');
6853
+ const knowledgeOnly = flags.has('knowledge-only');
6854
+ if (knowledgeOnly && !flags.has('workspace')) {
6855
+ throw new Error('Demo knowledge-only setup requires an explicit private --workspace outside Git.');
6856
+ }
6696
6857
  const workspace = await realpath(String(flags.get('workspace') || '.'));
6697
6858
  const root = await gitValue(workspace, ['rev-parse', '--show-toplevel']);
6698
- if (!root)
6699
- throw new Error('Demo device setup must run inside the exact Git repository.');
6700
- const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
6701
- if (!remote || normalizeGitRemoteIdentity(remote) !== normalizedRepository) {
6702
- throw new Error('Local Git remote does not match the private Demo repository binding.');
6859
+ if (knowledgeOnly) {
6860
+ if (root)
6861
+ throw new Error('Demo knowledge-only workspace must be outside every Git checkout.');
6862
+ }
6863
+ else {
6864
+ if (!root)
6865
+ throw new Error('Demo device setup must run inside the exact Git repository.');
6866
+ const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
6867
+ if (!remote || normalizeGitRemoteIdentity(remote) !== normalizedRepository) {
6868
+ throw new Error('Local Git remote does not match the private Demo repository binding.');
6869
+ }
6703
6870
  }
6704
6871
  const watchControl = ['watch-enable', 'watch-status', 'watch-disable'].includes(String(subcommand));
6705
6872
  const signingProof = ['signing-client-proof', 'signing-owner-proof'].includes(String(subcommand));
6706
6873
  if (signingProof && subcommand === 'signing-owner-proof' && flags.has('submit')) {
6707
6874
  throw new Error('Owner signing approval requires explicit browser confirmation.');
6708
6875
  }
6709
- if (watchControl && await realpath(root) !== workspace) {
6876
+ if (watchControl && !knowledgeOnly && await realpath(root) !== workspace) {
6710
6877
  throw new Error('Demo watch setup must select the exact Git repository root.');
6711
6878
  }
6712
6879
  let transportOrigin = null;
@@ -6725,6 +6892,7 @@ export async function run(argv) {
6725
6892
  return { ok: true, stage: transportOrigin ? 'demo_transport_plan' : signingProof ? 'demo_signing_proof_plan'
6726
6893
  : watchControl ? 'demo_watch_plan' : 'demo_device_plan',
6727
6894
  organizationId, repositoryId, normalizedRepository, workspaceVerified: true,
6895
+ ...(knowledgeOnly ? { workspaceMode: 'knowledge_only' } : {}),
6728
6896
  repositoryPackageState: 'not_connected', ...(transportOrigin ? { transportOrigin } : {}) };
6729
6897
  const grant = subcommand === 'connect' ? required(flags, 'grant') : null;
6730
6898
  const scope = {
@@ -6746,7 +6914,8 @@ export async function run(argv) {
6746
6914
  if (!isLocalProviderId(provider))
6747
6915
  throw new Error('Demo watch provider must be auto, codex, claude, agy, or hermes.');
6748
6916
  const registration = { schema: 'dharma.demo-watch/v1',
6749
- hqUrl, organizationId, repositoryId, normalizedRepository, provider, workspace };
6917
+ hqUrl, organizationId, repositoryId, normalizedRepository, provider, workspace,
6918
+ ...(knowledgeOnly ? { knowledgeOnly: true } : {}) };
6750
6919
  const input = { home: dharmaHome(), registration, version: VERSION };
6751
6920
  const deps = {
6752
6921
  verify: async () => {
@@ -6781,6 +6950,7 @@ export async function run(argv) {
6781
6950
  : nativeSkillDirectory(provider);
6782
6951
  const cycle = () => withDemoDeviceLock(scope, async () => demoRepositoryPackage({ scope, workspace,
6783
6952
  statusOnly: subcommand === 'package-status', provider,
6953
+ knowledgeOnly,
6784
6954
  nativeSkillDirectory: demoNativeSkillDirectory }, await createDemoDeviceTransport(scope)));
6785
6955
  if (subcommand !== 'watch')
6786
6956
  return cycle();