synomem 0.8.0 → 0.9.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +47 -68
  3. package/dist/backend.d.ts +18 -6
  4. package/dist/backend.d.ts.map +1 -1
  5. package/dist/backend.js +55 -41
  6. package/dist/backend.js.map +1 -1
  7. package/dist/cli.d.ts +20 -33
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +1391 -1321
  10. package/dist/cli.js.map +1 -1
  11. package/dist/configure.d.ts +12 -46
  12. package/dist/configure.d.ts.map +1 -1
  13. package/dist/configure.js +51 -192
  14. package/dist/configure.js.map +1 -1
  15. package/dist/credentials.d.ts +73 -33
  16. package/dist/credentials.d.ts.map +1 -1
  17. package/dist/credentials.js +167 -43
  18. package/dist/credentials.js.map +1 -1
  19. package/dist/discover.d.ts +8 -35
  20. package/dist/discover.d.ts.map +1 -1
  21. package/dist/discover.js +42 -38
  22. package/dist/discover.js.map +1 -1
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +4 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/import.d.ts +3 -0
  28. package/dist/import.d.ts.map +1 -1
  29. package/dist/import.js +3 -0
  30. package/dist/import.js.map +1 -1
  31. package/dist/index.d.ts +10 -8
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +6 -5
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/index.d.ts +18 -18
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +402 -231
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp-server.d.ts +5 -1
  40. package/dist/mcp-server.d.ts.map +1 -1
  41. package/dist/mcp-server.js +27 -105
  42. package/dist/mcp-server.js.map +1 -1
  43. package/dist/oauth.d.ts +31 -33
  44. package/dist/oauth.d.ts.map +1 -1
  45. package/dist/oauth.js +178 -125
  46. package/dist/oauth.js.map +1 -1
  47. package/dist/profiles.d.ts +243 -0
  48. package/dist/profiles.d.ts.map +1 -0
  49. package/dist/profiles.js +465 -0
  50. package/dist/profiles.js.map +1 -0
  51. package/dist/project.d.ts +8 -39
  52. package/dist/project.d.ts.map +1 -1
  53. package/dist/project.js +36 -94
  54. package/dist/project.js.map +1 -1
  55. package/dist/remote.d.ts +24 -17
  56. package/dist/remote.d.ts.map +1 -1
  57. package/dist/remote.js +54 -52
  58. package/dist/remote.js.map +1 -1
  59. package/dist/resolvers.d.ts +47 -0
  60. package/dist/resolvers.d.ts.map +1 -0
  61. package/dist/resolvers.js +255 -0
  62. package/dist/resolvers.js.map +1 -0
  63. package/dist/service.d.ts +3 -2
  64. package/dist/service.d.ts.map +1 -1
  65. package/dist/skill-install.d.ts +4 -6
  66. package/dist/skill-install.d.ts.map +1 -1
  67. package/dist/skill-install.js +13 -12
  68. package/dist/skill-install.js.map +1 -1
  69. package/dist/types.d.ts +45 -15
  70. package/dist/types.d.ts.map +1 -1
  71. package/docs/cli.md +173 -196
  72. package/docs/mcp.md +69 -65
  73. package/package.json +1 -1
  74. package/skills/synomem/SKILL.md +29 -26
  75. package/skills/synomem/references/examples.md +13 -11
  76. package/src/backend.ts +66 -64
  77. package/src/cli.ts +2131 -2222
  78. package/src/configure.ts +62 -241
  79. package/src/credentials.ts +208 -84
  80. package/src/discover.ts +53 -59
  81. package/src/errors.ts +4 -0
  82. package/src/import.ts +5 -0
  83. package/src/index.ts +15 -19
  84. package/src/mcp/index.ts +473 -277
  85. package/src/mcp-server.ts +32 -114
  86. package/src/oauth.ts +229 -130
  87. package/src/profiles.ts +644 -0
  88. package/src/project.ts +42 -108
  89. package/src/remote.ts +69 -63
  90. package/src/resolvers.ts +299 -0
  91. package/src/service.ts +2 -7
  92. package/src/skill-install.ts +17 -18
  93. package/src/types.ts +40 -15
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Context resolvers: the one place an operation's workspace/actor target is chosen
3
+ * (identity contract §6, §6.1; plan §7 "Implementation boundary").
4
+ *
5
+ * Every MCP tool and CLI command asks a resolver for a service bound to exactly one
6
+ * context, per call. Nothing holds a mutable "current actor" or "current workspace":
7
+ * a resolved binding is immutable, so interleaved calls for two contexts cannot affect
8
+ * each other's attribution. The resolver narrows (pins a context, restricts to a
9
+ * preset); the server alone authorizes.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import { SynomemClient } from './client.js';
13
+ import { describeIdentity, discoverContexts } from './discover.js';
14
+ import { SynomemError } from './errors.js';
15
+ import { bearerFrom, RemoteSynomemService } from './remote.js';
16
+ import type { RemoteCredential, RemoteCredentialSource } from './remote.js';
17
+ import type { SynomemService } from './service.js';
18
+ import type {
19
+ ActorIdentity,
20
+ ContextListing,
21
+ ContextSummary,
22
+ EffectiveContext,
23
+ IdentityDescription,
24
+ SynomemClientOptions,
25
+ } from './types.js';
26
+
27
+ export type { RemoteCredentialSource } from './remote.js';
28
+
29
+ export interface ResolvedContext {
30
+ service: SynomemService;
31
+ context: EffectiveContext;
32
+ }
33
+
34
+ export interface ContextResolver {
35
+ /** `fixed`: omitting a context selects the one pinned target. */
36
+ mode(): 'fixed' | 'explicit' | 'unknown';
37
+ /** Resolve one operation's binding. `undefined` or `'default'` selects the fixed context. */
38
+ resolve(contextId?: string): Promise<ResolvedContext>;
39
+ list(): Promise<ContextListing>;
40
+ describe?(): Promise<IdentityDescription | undefined>;
41
+ close?(): Promise<void>;
42
+ }
43
+
44
+ /** The selector a model may pass to mean "the fixed context", e.g. in resource URIs. */
45
+ export const DEFAULT_CONTEXT = 'default';
46
+
47
+ const listingTtlMs = 30_000;
48
+
49
+ function normalize(contextId: string | undefined): string | undefined {
50
+ const value = contextId?.trim();
51
+ return !value || value === DEFAULT_CONTEXT ? undefined : value;
52
+ }
53
+
54
+ function forbidden(): SynomemError {
55
+ // The same message whether the target is missing or merely unpermitted: an
56
+ // authorization failure must not reveal that an inaccessible context exists.
57
+ return new SynomemError(
58
+ 'CONTEXT_FORBIDDEN',
59
+ 'That context is not available to this credential. Call synomem_context_list to see the ones that are.',
60
+ );
61
+ }
62
+
63
+ function required(): SynomemError {
64
+ return new SynomemError(
65
+ 'CONTEXT_REQUIRED',
66
+ 'This connection can act as more than one workspace/actor. Pass contextId: call synomem_context_list and choose the context that matches what the user asked for.',
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Stable local context id: derived from the store's persistent workspace id and the
72
+ * canonical actor — never from a display name — so it survives renames and moving the
73
+ * store, and an unrelated store with a same-named agent gets a different id.
74
+ */
75
+ export function localContextId(storeWorkspaceId: string, actor: ActorIdentity): string {
76
+ const digest = createHash('sha256')
77
+ .update(`${storeWorkspaceId}\0${actor.kind}\0${actor.id}`)
78
+ .digest('hex');
79
+ return `lctx_${digest.slice(0, 24)}`;
80
+ }
81
+
82
+ /**
83
+ * A remote (hosted API) resolver over one credential.
84
+ *
85
+ * `pinnedContextId` makes a fixed profile: only that target, whatever else the grant
86
+ * allows. `allowedContextIds` makes an explicit preset: only those targets may be
87
+ * selected. Either way the API re-authorizes every request; these only narrow.
88
+ */
89
+ export function createRemoteResolver(options: {
90
+ baseUrl: string;
91
+ credential: RemoteCredentialSource | RemoteCredential;
92
+ pinnedContextId?: string;
93
+ allowedContextIds?: string[];
94
+ fetch?: typeof fetch;
95
+ }): ContextResolver {
96
+ const bearer = bearerFrom(options.credential);
97
+ const pinned = normalize(options.pinnedContextId);
98
+ const allowed = options.allowedContextIds?.length
99
+ ? new Set(options.allowedContextIds)
100
+ : undefined;
101
+ let cached: { listing: ContextListing; at: number } | undefined;
102
+ const services = new Map<string, Promise<RemoteSynomemService>>();
103
+
104
+ const accessToken = async (): Promise<string> => {
105
+ const token = await bearer();
106
+ if (!token)
107
+ throw new SynomemError('AUTH_REQUIRED', 'Remote Synomem authentication is required.');
108
+ return token;
109
+ };
110
+
111
+ const serverListing = async (fresh = false): Promise<ContextListing> => {
112
+ if (!fresh && cached && Date.now() - cached.at < listingTtlMs) return cached.listing;
113
+ const listing = await discoverContexts({
114
+ baseUrl: options.baseUrl,
115
+ accessToken: await accessToken(),
116
+ ...(options.fetch ? { fetch: options.fetch } : {}),
117
+ });
118
+ cached = { listing, at: Date.now() };
119
+ return listing;
120
+ };
121
+
122
+ const visible = (listing: ContextListing): ContextListing => {
123
+ if (pinned) {
124
+ return {
125
+ mode: 'fixed',
126
+ fixedContextId: pinned,
127
+ contexts: listing.contexts.filter((entry) => entry.contextId === pinned),
128
+ nextCursor: null,
129
+ };
130
+ }
131
+ if (allowed) {
132
+ return {
133
+ mode: 'explicit',
134
+ fixedContextId: null,
135
+ contexts: listing.contexts.filter((entry) => allowed.has(entry.contextId)),
136
+ nextCursor: null,
137
+ };
138
+ }
139
+ return listing;
140
+ };
141
+
142
+ const summaryFor = async (contextId: string): Promise<ContextSummary> => {
143
+ let found = (await serverListing()).contexts.find((entry) => entry.contextId === contextId);
144
+ // A newly eligible target (a future-workspace rule, a fresh binding) may postdate the
145
+ // cached listing; look once more before refusing.
146
+ if (!found) {
147
+ found = (await serverListing(true)).contexts.find((entry) => entry.contextId === contextId);
148
+ }
149
+ if (!found) throw forbidden();
150
+ return found;
151
+ };
152
+
153
+ const serviceFor = (summary: ContextSummary): Promise<RemoteSynomemService> => {
154
+ let pending = services.get(summary.contextId);
155
+ if (!pending) {
156
+ pending = (async () => {
157
+ const service = new RemoteSynomemService({
158
+ baseUrl: options.baseUrl,
159
+ workspaceId: summary.workspaceId,
160
+ contextId: summary.contextId,
161
+ credential: options.credential,
162
+ ...(options.fetch ? { fetch: options.fetch } : {}),
163
+ });
164
+ await service.init();
165
+ return service;
166
+ })();
167
+ services.set(summary.contextId, pending);
168
+ pending.catch(() => services.delete(summary.contextId));
169
+ }
170
+ return pending;
171
+ };
172
+
173
+ return {
174
+ mode() {
175
+ if (pinned) return 'fixed';
176
+ if (allowed) return 'explicit';
177
+ return cached ? cached.listing.mode : 'unknown';
178
+ },
179
+ async resolve(contextId) {
180
+ const requested = normalize(contextId);
181
+ let target: string;
182
+ if (pinned) {
183
+ if (requested && requested !== pinned) throw forbidden();
184
+ target = pinned;
185
+ } else if (requested) {
186
+ if (allowed && !allowed.has(requested)) throw forbidden();
187
+ target = requested;
188
+ } else {
189
+ const listing = await serverListing();
190
+ if (listing.mode === 'fixed' && listing.fixedContextId && !allowed) {
191
+ target = listing.fixedContextId;
192
+ } else {
193
+ throw required();
194
+ }
195
+ }
196
+ const summary = await summaryFor(target);
197
+ const service = await serviceFor(summary);
198
+ return {
199
+ service,
200
+ context: {
201
+ contextId: summary.contextId,
202
+ organizationId: summary.organizationId,
203
+ workspaceId: summary.workspaceId,
204
+ actor: service.actor,
205
+ },
206
+ };
207
+ },
208
+ async list() {
209
+ return visible(await serverListing());
210
+ },
211
+ async describe() {
212
+ return await describeIdentity({
213
+ baseUrl: options.baseUrl,
214
+ accessToken: await accessToken(),
215
+ ...(options.fetch ? { fetch: options.fetch } : {}),
216
+ });
217
+ },
218
+ async close() {
219
+ const pending = [...services.values()];
220
+ services.clear();
221
+ await Promise.allSettled(pending.map(async (service) => (await service).close()));
222
+ },
223
+ };
224
+ }
225
+
226
+ /**
227
+ * A local (SQLite) resolver: fixed mode, one store, one canonical actor.
228
+ */
229
+ export function createLocalResolver(
230
+ options: { home: string; actor: ActorIdentity } & Omit<SynomemClientOptions, 'home' | 'actor'>,
231
+ ): ContextResolver {
232
+ let ready: Promise<ResolvedContext> | undefined;
233
+ const open = (): Promise<ResolvedContext> => {
234
+ ready ??= (async () => {
235
+ const client = new SynomemClient({ ...options, home: options.home, actor: options.actor });
236
+ await client.init();
237
+ const capabilities = await client.capabilities();
238
+ // The canonical actor (a handle resolves to its opaque id), never the requested one.
239
+ const actor = client.actor;
240
+ return {
241
+ service: client,
242
+ context: {
243
+ contextId: localContextId(capabilities.binding.workspaceId, actor),
244
+ organizationId: null,
245
+ workspaceId: capabilities.binding.workspaceId,
246
+ actor,
247
+ },
248
+ };
249
+ })();
250
+ ready.catch(() => {
251
+ ready = undefined;
252
+ });
253
+ return ready;
254
+ };
255
+
256
+ return {
257
+ mode: () => 'fixed',
258
+ async resolve(contextId) {
259
+ const bound = await open();
260
+ const requested = normalize(contextId);
261
+ if (requested && requested !== bound.context.contextId) throw forbidden();
262
+ return bound;
263
+ },
264
+ async list() {
265
+ const { context } = await open();
266
+ return {
267
+ mode: 'fixed',
268
+ fixedContextId: context.contextId,
269
+ contexts: [
270
+ {
271
+ contextId: context.contextId,
272
+ organizationId: null,
273
+ workspaceId: context.workspaceId,
274
+ actor: context.actor,
275
+ actions: ['synomem:read', 'synomem:write'],
276
+ source: 'local',
277
+ },
278
+ ],
279
+ nextCursor: null,
280
+ };
281
+ },
282
+ async describe() {
283
+ const { context } = await open();
284
+ return {
285
+ account: { id: 'local' },
286
+ organizationId: null,
287
+ connection: { id: 'local', label: options.home, kind: 'local' },
288
+ grant: null,
289
+ fixedContextId: context.contextId,
290
+ effectiveContext: context,
291
+ };
292
+ },
293
+ async close() {
294
+ const pending = ready;
295
+ ready = undefined;
296
+ if (pending) await (await pending.catch(() => undefined))?.service.close();
297
+ },
298
+ };
299
+ }
package/src/service.ts CHANGED
@@ -48,7 +48,6 @@ import type {
48
48
  UpdateTopicInput,
49
49
  TopicListInput,
50
50
  TopicResolution,
51
- WorkspaceIdentity,
52
51
  } from './types.js';
53
52
 
54
53
  export interface SynomemServiceCapabilities {
@@ -56,6 +55,8 @@ export interface SynomemServiceCapabilities {
56
55
  binding: {
57
56
  workspaceId: string;
58
57
  actor: ActorIdentity;
58
+ /** The stable context this service is bound to, when the backend reports one. */
59
+ contextId?: string;
59
60
  };
60
61
  administration: {
61
62
  agentCreationViaMcp: boolean;
@@ -245,12 +246,6 @@ export interface SynomemService extends SynomemDomainService {
245
246
  capabilities(): Promise<SynomemServiceCapabilities>;
246
247
  info(): Promise<SynomemServiceInfo>;
247
248
  getCanonicalEvent(id: string): Promise<SynomemEvent | undefined>;
248
- /*
249
- * Optional, because only a hosted Synomem Cloud backend has an
250
- * organization with more than one workspace an account can belong to.
251
- * Local SQLite has no membership model to report on.
252
- */
253
- workspaces?(): Promise<WorkspaceIdentity>;
254
249
  }
255
250
 
256
251
  export type SynomemServiceFactory = (options: SynomemClientOptions) => SynomemService;
@@ -14,7 +14,6 @@ import { homedir } from 'node:os';
14
14
  import { dirname, join, resolve } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { SynomemError } from './errors.js';
17
- import { actorSchema } from './schemas.js';
18
17
  import { packageVersion } from './version.js';
19
18
 
20
19
  export const skillRuntimeNames = [
@@ -58,13 +57,11 @@ export interface SkillOptions {
58
57
  force?: boolean;
59
58
  link?: boolean;
60
59
  /**
61
- * The agent this installation binds to.
62
- *
63
- * Only the canonical ID is written into the generated registration command.
64
- * The display name and kind are read from the agent's profile at startup, so
65
- * a harness cannot sign another agent's name to work it did.
60
+ * The profile the generated MCP registration launches (`synomem mcp
61
+ * --profile <name>`). The profile, not the command line, decides the acting
62
+ * identity, so a harness cannot name another agent's identity itself.
66
63
  */
67
- agentId?: string;
64
+ profile?: string;
68
65
  userHome?: string;
69
66
  env?: NodeJS.ProcessEnv;
70
67
  source?: string;
@@ -166,24 +163,26 @@ function shellQuote(value: string): string {
166
163
  return `'${value.replaceAll("'", `'\\''`)}'`;
167
164
  }
168
165
 
169
- function mcpCommand(runtime: SkillRuntime, agentId?: string): string | undefined {
170
- if (!agentId) return undefined;
171
- const identity = actorSchema.parse({ kind: 'agent', id: agentId });
172
- const args = ['--agent-id', identity.id];
166
+ function mcpCommand(runtime: SkillRuntime, profile?: string): string | undefined {
167
+ if (!profile) return undefined;
168
+ if (!/^[a-z0-9][a-z0-9._-]{0,62}$/i.test(profile)) {
169
+ throw new SynomemError('INVALID_INPUT', `Invalid profile name: ${profile}`);
170
+ }
171
+ const args = ['mcp', '--profile', profile];
173
172
  if (runtime === 'codex') {
174
- return `codex mcp add synomem -- synomem-mcp ${args.map(shellQuote).join(' ')}`;
173
+ return `codex mcp add synomem -- synomem ${args.map(shellQuote).join(' ')}`;
175
174
  }
176
175
  if (runtime === 'claude') {
177
- return `claude mcp add --scope user synomem -- synomem-mcp ${args.map(shellQuote).join(' ')}`;
176
+ return `claude mcp add --scope user synomem -- synomem ${args.map(shellQuote).join(' ')}`;
178
177
  }
179
178
  if (runtime === 'hermes') {
180
- return `hermes mcp add synomem --command synomem-mcp --args ${args.map(shellQuote).join(' ')}`;
179
+ return `hermes mcp add synomem --command synomem --args ${args.map(shellQuote).join(' ')}`;
181
180
  }
182
181
  if (runtime === 'openclaw') {
183
- return `openclaw mcp add synomem --command synomem-mcp ${args.map((item) => `--arg=${shellQuote(item)}`).join(' ')}`;
182
+ return `openclaw mcp add synomem --command synomem ${args.map((item) => `--arg=${shellQuote(item)}`).join(' ')}`;
184
183
  }
185
184
  if (runtime === 'grok') {
186
- return `grok mcp add synomem -- synomem-mcp ${args.map(shellQuote).join(' ')}`;
185
+ return `grok mcp add synomem -- synomem ${args.map(shellQuote).join(' ')}`;
187
186
  }
188
187
  return undefined;
189
188
  }
@@ -255,7 +254,7 @@ export function skillStatus(options: SkillOptions = {}): SkillOperationResult {
255
254
  packageVersion: packageVersion(),
256
255
  locations,
257
256
  mcpCommands: locations
258
- .map((location) => mcpCommand(location.runtime, options.agentId))
257
+ .map((location) => mcpCommand(location.runtime, options.profile))
259
258
  .filter((value): value is string => Boolean(value)),
260
259
  };
261
260
  }
@@ -323,7 +322,7 @@ export function formatSkillResult(
323
322
  lines.push('', 'Dry run only. Re-run with --yes to apply.');
324
323
  if (result.mcpCommands.length) lines.push('', 'MCP registration:', ...result.mcpCommands);
325
324
  else if (operation === 'install') {
326
- lines.push('', 'Tip: add --agent <agent-id> to print MCP registration commands.');
325
+ lines.push('', 'Tip: add --profile <name> to print MCP registration commands.');
327
326
  }
328
327
  return lines.join('\n');
329
328
  }
package/src/types.ts CHANGED
@@ -822,25 +822,50 @@ export interface DoctorResult {
822
822
  diagnostics: Diagnostic[];
823
823
  }
824
824
  /**
825
- * Every workspace this credential's account belongs to, and which of them
826
- * are reachable right now without a new token. A human actor's token
827
- * authorizes an organization, not permanently one workspace: any
828
- * `addressableWithThisToken: true` entry can be reached on the very next
829
- * request by sending it as `Synomem-Workspace-Id` — no re-authentication.
830
- * An agent-bound token has only ever one such entry, its own.
825
+ * The one workspace/actor target an operation actually ran as (identity contract §4).
826
+ * `contextId` is a stable, grant-independent identifier: `ctx_…` for a hosted target,
827
+ * `lctx_…` for a local SQLite target.
831
828
  */
832
- export interface WorkspaceIdentity {
829
+ export interface EffectiveContext {
830
+ contextId: string;
831
+ organizationId: string | null;
832
+ workspaceId: string;
833
833
  actor: ActorIdentity;
834
- /** The workspace this credential currently addresses. */
834
+ connectionId?: string;
835
+ }
836
+
837
+ /** One entry of `GET /v1/contexts`: a target this credential may currently use. */
838
+ export interface ContextSummary {
839
+ contextId: string;
840
+ organizationId: string | null;
835
841
  workspaceId: string;
836
- roles: string[];
837
- scopes: string[];
838
- workspaces: Array<{
842
+ workspaceName?: string;
843
+ actor: ActorIdentity & { handle?: string };
844
+ actions: string[];
845
+ source: 'target' | 'rule' | 'local';
846
+ }
847
+
848
+ /** `GET /v1/contexts`. `fixed` means omitting a context selects `fixedContextId`. */
849
+ export interface ContextListing {
850
+ mode: 'fixed' | 'explicit';
851
+ fixedContextId: string | null;
852
+ contexts: ContextSummary[];
853
+ nextCursor?: string | null;
854
+ }
855
+
856
+ /** `GET /v1/identity`: the credential, its grant, and (optionally) the effective context. */
857
+ export interface IdentityDescription {
858
+ account: { id: string };
859
+ organizationId: string | null;
860
+ connection: { id: string; label: string; kind: 'oauth' | 'access_key' | 'local' } | null;
861
+ grant: {
839
862
  id: string;
840
- displayName: string;
841
- roles: string[];
842
- addressableWithThisToken: boolean;
843
- }>;
863
+ version: number;
864
+ mode: 'fixed' | 'explicit';
865
+ actions: string[];
866
+ } | null;
867
+ fixedContextId: string | null;
868
+ effectiveContext: EffectiveContext | null;
844
869
  }
845
870
  /**
846
871
  * What the projected files on disk look like next to what they should be.