@intentic/sandbox-contract 1.302.0 → 1.304.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/dist/contracts/agent.contract.d.ts +6 -9
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +207 -433
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +6 -24
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/areas.contract.d.ts +24 -0
- package/dist/contracts/areas.contract.d.ts.map +1 -0
- package/dist/contracts/areas.contract.js +32 -0
- package/dist/contracts/areas.contract.js.map +1 -0
- package/dist/contracts/automations.contract.d.ts +3 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +0 -46
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +131 -0
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.js +50 -0
- package/dist/contracts/endpoints.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +20 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.js +1 -0
- package/dist/contracts/providers.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +98 -95
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +0 -1
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +108 -6
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +19 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +25 -12
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +1 -3
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +1 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/system-events.d.ts +31 -6
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +2 -6
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +5 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +489 -544
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +1 -1
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/policy/fence-paths.d.ts +10 -0
- package/dist/policy/fence-paths.d.ts.map +1 -0
- package/dist/policy/fence-paths.js +58 -0
- package/dist/policy/fence-paths.js.map +1 -0
- package/dist/policy/persona-home.d.ts +10 -0
- package/dist/policy/persona-home.d.ts.map +1 -0
- package/dist/policy/persona-home.js +11 -0
- package/dist/policy/persona-home.js.map +1 -0
- package/dist/schemas/agents.d.ts +54 -19
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +11 -22
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/areas.d.ts +21 -0
- package/dist/schemas/areas.d.ts.map +1 -0
- package/dist/schemas/areas.js +32 -0
- package/dist/schemas/areas.js.map +1 -0
- package/dist/schemas/automations.d.ts +20 -3
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +2 -0
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/capabilities.d.ts +11 -46
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +15 -14
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +5 -1
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/providers/usage.d.ts +4 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +4 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/settings.d.ts +98 -3
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +46 -14
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/shared.d.ts +2 -0
- package/dist/schemas/shared.d.ts.map +1 -1
- package/dist/schemas/shared.js +3 -3
- package/dist/schemas/shared.js.map +1 -1
- package/dist/schemas/turn-break.d.ts +32 -0
- package/dist/schemas/turn-break.d.ts.map +1 -0
- package/dist/schemas/turn-break.js +16 -0
- package/dist/schemas/turn-break.js.map +1 -0
- package/dist/state/definition.d.ts +36 -56
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +1 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +5 -0
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +1 -0
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +3 -7
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/time/zone.d.ts +27 -0
- package/dist/time/zone.d.ts.map +1 -0
- package/dist/time/zone.js +35 -0
- package/dist/time/zone.js.map +1 -0
- package/package.json +16 -5
- package/src/contracts/agents.contract.ts +7 -30
- package/src/contracts/areas.contract.ts +42 -0
- package/src/contracts/endpoints.contract.ts +82 -0
- package/src/contracts/providers.contract.ts +5 -0
- package/src/contracts/settings.contract.ts +28 -0
- package/src/events/agent-events.ts +4 -1
- package/src/events/transcript.ts +9 -1
- package/src/index.ts +20 -1
- package/src/models/agent-catalog.ts +1 -1
- package/src/policy/fence-paths.test.ts +114 -0
- package/src/policy/fence-paths.ts +99 -0
- package/src/policy/persona-home.test.ts +52 -0
- package/src/policy/persona-home.ts +39 -0
- package/src/schemas/agents.ts +22 -35
- package/src/schemas/areas.test.ts +40 -0
- package/src/schemas/areas.ts +49 -0
- package/src/schemas/automations.ts +8 -0
- package/src/schemas/capabilities.ts +37 -19
- package/src/schemas/personas.ts +15 -4
- package/src/schemas/providers/usage.ts +9 -0
- package/src/schemas/settings.ts +98 -26
- package/src/schemas/shared.ts +6 -5
- package/src/schemas/turn-break.ts +49 -0
- package/src/state/history-state.ts +4 -0
- package/src/state/workspace-state.test.ts +7 -3
- package/src/state/workspace-state.ts +9 -2
- package/src/text/transcript-fold.test.ts +3 -5
- package/src/text/transcript-fold.ts +3 -7
- package/src/time/zone.test.ts +90 -0
- package/src/time/zone.ts +118 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
AgentArchiveSchema,
|
|
5
5
|
AgentAssignSchema,
|
|
6
6
|
AgentAutoLandSchema,
|
|
7
|
+
AgentBreakPolicySchema,
|
|
7
8
|
AgentFileDiffQuerySchema,
|
|
8
9
|
AgentIdSchema,
|
|
9
10
|
AgentToolChildrenQuerySchema,
|
|
@@ -13,10 +14,7 @@ import {
|
|
|
13
14
|
AgentPlaceSchema,
|
|
14
15
|
AgentReactSchema,
|
|
15
16
|
AgentRenameSchema,
|
|
16
|
-
AgentResumeAfterLimitSchema,
|
|
17
17
|
AgentStopWatchingSchema,
|
|
18
|
-
AgentMoveAfterLimitSchema,
|
|
19
|
-
AgentResumeAfterOutageSchema,
|
|
20
18
|
AgentsArchivedSchema,
|
|
21
19
|
AgentSearchQuerySchema,
|
|
22
20
|
AgentSearchResultSchema,
|
|
@@ -125,37 +123,16 @@ export const agentsContract = {
|
|
|
125
123
|
.input(AgentAutoLandSchema)
|
|
126
124
|
.output(AgentSummarySchema),
|
|
127
125
|
// Legal for a workspace conversation too (unlike autoLand): an outage kills a main-tree chat just as readily.
|
|
128
|
-
|
|
126
|
+
// Also offered on the card, not just in chat: a limit reopens hours later, so the decider is usually at a board.
|
|
127
|
+
breakPolicy: oc
|
|
129
128
|
.route({
|
|
130
129
|
method: "POST",
|
|
131
|
-
path: "/agents/{id}/
|
|
132
|
-
summary: "
|
|
130
|
+
path: "/agents/{id}/break-policy",
|
|
131
|
+
summary: "What this conversation does when a turn stops before it finished",
|
|
133
132
|
description:
|
|
134
|
-
"
|
|
133
|
+
"One answer per ending — a spent usage limit, a provider outage, a turn that stopped short — overriding the sandbox-wide policy for one conversation; clear it to follow the default again. The answers are mutually exclusive by construction, so nothing here can arm two automations over the same wall. Every ending starts at `wait` unless asked otherwise, because a re-run spends the user's own allowance on a turn they sent once.",
|
|
135
134
|
})
|
|
136
|
-
.input(
|
|
137
|
-
.output(AgentSummarySchema),
|
|
138
|
-
// Also offered on the card, not just chat: a limit reopens hours later, so the decider is usually at a board.
|
|
139
|
-
resumeAfterLimit: oc
|
|
140
|
-
.route({
|
|
141
|
-
method: "POST",
|
|
142
|
-
path: "/agents/{id}/resume-after-limit",
|
|
143
|
-
summary: "Whether this conversation sends itself again when its allowance comes back",
|
|
144
|
-
description:
|
|
145
|
-
"Overrides the sandbox-wide setting for one conversation; clear it to follow the default again. Off unless asked for, because the allowance is the user's own budget and a turn that spends it the moment it reopens is not a decision to make on their behalf.",
|
|
146
|
-
})
|
|
147
|
-
.input(AgentResumeAfterLimitSchema)
|
|
148
|
-
.output(AgentSummarySchema),
|
|
149
|
-
// Moves the held turn the moment the refusal lands, not on the next attempt.
|
|
150
|
-
moveAfterLimit: oc
|
|
151
|
-
.route({
|
|
152
|
-
method: "POST",
|
|
153
|
-
path: "/agents/{id}/move-after-limit",
|
|
154
|
-
summary: "Whether this conversation moves to another account when its allowance is spent",
|
|
155
|
-
description:
|
|
156
|
-
"Overrides the sandbox-wide setting for one conversation; clear it to follow the default again. A move spends a second account of the same provider on this conversation's behalf, so it is off unless asked for.",
|
|
157
|
-
})
|
|
158
|
-
.input(AgentMoveAfterLimitSchema)
|
|
135
|
+
.input(AgentBreakPolicySchema)
|
|
159
136
|
.output(AgentSummarySchema),
|
|
160
137
|
seen: oc
|
|
161
138
|
.route({
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { OkSchema } from "../schemas/shared.js";
|
|
3
|
+
import { AreaIdParamSchema, AreaSchema, AreasListSchema } from "../schemas/areas.js";
|
|
4
|
+
|
|
5
|
+
// Named parts of the workspace (AreaSchema), the unit a member's reach is granted in. Reading them is a member's own
|
|
6
|
+
// business — a fenced person is shown the name of the fence they are behind — but writing one changes who sees what,
|
|
7
|
+
// which is the one decision a revokable grant must not make, so the two writes below are owner-gated in-route the way
|
|
8
|
+
// the roster is.
|
|
9
|
+
export const areasContract = {
|
|
10
|
+
list: oc
|
|
11
|
+
.route({
|
|
12
|
+
method: "GET",
|
|
13
|
+
path: "/areas",
|
|
14
|
+
summary: "The named parts of the workspace",
|
|
15
|
+
description:
|
|
16
|
+
"Each area with the folders it admits. Access is granted in these rather than in folder lists per person, so widening what a team sees is one edit here instead of one edit per member.",
|
|
17
|
+
})
|
|
18
|
+
.output(AreasListSchema),
|
|
19
|
+
// Upsert by id, re-saving the same id edits that area.
|
|
20
|
+
save: oc
|
|
21
|
+
.route({
|
|
22
|
+
method: "POST",
|
|
23
|
+
path: "/areas",
|
|
24
|
+
summary: "Create or edit an area",
|
|
25
|
+
description:
|
|
26
|
+
"Writes the whole area; sending an id that exists edits it. Editing the folders of an area people already hold changes what those people see on their next request, which is why this is the sandbox owner's to do and why the file it writes is tracked and reviewable.",
|
|
27
|
+
})
|
|
28
|
+
.input(AreaSchema)
|
|
29
|
+
.output(OkSchema),
|
|
30
|
+
// Refused while anyone still holds it: a member row pointing at an area that no longer exists would either fail
|
|
31
|
+
// open (everything) or fail shut (nothing), and neither is a decision anybody made.
|
|
32
|
+
remove: oc
|
|
33
|
+
.route({
|
|
34
|
+
method: "DELETE",
|
|
35
|
+
path: "/areas/{id}",
|
|
36
|
+
summary: "Delete an area",
|
|
37
|
+
description:
|
|
38
|
+
"Removes the name and the folders behind it. Refused while a member still points at it, since nobody chose what such a row should then mean; move them onto another area first, or off areas entirely.",
|
|
39
|
+
})
|
|
40
|
+
.input(AreaIdParamSchema)
|
|
41
|
+
.output(OkSchema),
|
|
42
|
+
};
|
|
@@ -27,6 +27,65 @@ export const TrialStatusSchema = z.object({
|
|
|
27
27
|
});
|
|
28
28
|
export type TrialStatusResponse = z.infer<typeof TrialStatusSchema>;
|
|
29
29
|
|
|
30
|
+
// Whether this container can reach an NVIDIA GPU, and how sure we are. The three states are not a tri-state boolean:
|
|
31
|
+
// `absent` means nobody has asked yet, so the switch is still worth offering, while `unsupported` means the host's
|
|
32
|
+
// Docker answered and has no nvidia runtime, which is a sentence to print rather than a switch to offer. Before the
|
|
33
|
+
// grant the sandbox cannot see VRAM at all — only after it can `gpuMemoryBytes` be anything but zero.
|
|
34
|
+
export const LocalModelGpuSchema = z.enum(["granted", "unsupported", "absent"]);
|
|
35
|
+
export type LocalModelGpu = z.infer<typeof LocalModelGpuSchema>;
|
|
36
|
+
|
|
37
|
+
// One window rung priced against one model, so a surface never has to redo the arithmetic the start check will.
|
|
38
|
+
const LocalModelWindowFitSchema = z.object({
|
|
39
|
+
tokens: z.number().int().positive(),
|
|
40
|
+
// Weights + KV cache + the runtime's own floor: the same estimate the admission check refuses a start on.
|
|
41
|
+
totalBytes: z.number().int().nonnegative(),
|
|
42
|
+
fits: z.boolean(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const LocalModelOptionFitSchema = z.object({
|
|
46
|
+
model: z.string(),
|
|
47
|
+
label: z.string(),
|
|
48
|
+
tier: z.enum(["instant", "work"]),
|
|
49
|
+
weightsBytes: z.number().int().nonnegative(),
|
|
50
|
+
// Already in the workspace cache, so adding it costs no download; true for anything a previous card fetched.
|
|
51
|
+
held: z.boolean(),
|
|
52
|
+
windows: z.array(LocalModelWindowFitSchema),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// What the weights the connect view offers to fetch ahead of being asked for are doing right now. Read-only: the POST
|
|
56
|
+
// is what starts and stops it.
|
|
57
|
+
export const LocalModelPrefetchSchema = z.object({
|
|
58
|
+
model: z.string(),
|
|
59
|
+
state: z.enum(["idle", "downloading", "held", "failed"]),
|
|
60
|
+
receivedBytes: z.number().int().nonnegative(),
|
|
61
|
+
// Zero where the server never said how big the file is; a progress bar must not invent a denominator.
|
|
62
|
+
totalBytes: z.number().int().nonnegative(),
|
|
63
|
+
detail: z.string().optional(),
|
|
64
|
+
});
|
|
65
|
+
export type LocalModelPrefetch = z.infer<typeof LocalModelPrefetchSchema>;
|
|
66
|
+
|
|
67
|
+
export const LocalModelFitSchema = z.object({
|
|
68
|
+
// What this container may actually use: its cgroup cap where it has one, the machine's own RAM otherwise.
|
|
69
|
+
memoryBytes: z.number().int().nonnegative(),
|
|
70
|
+
// True when the number above is a container limit rather than the machine's: worth saying, since the host may look
|
|
71
|
+
// much larger than what a model can have.
|
|
72
|
+
memoryCapped: z.boolean(),
|
|
73
|
+
gpu: LocalModelGpuSchema,
|
|
74
|
+
gpuMemoryBytes: z.number().int().nonnegative(),
|
|
75
|
+
// The budget a start is actually admitted against, not the raw total: a view that sizes against anything else can
|
|
76
|
+
// recommend a model the daemon then refuses.
|
|
77
|
+
budgetBytes: z.number().int().nonnegative(),
|
|
78
|
+
// Whether llama-server is in this image. False means the local lane costs a rebuild before it can serve anything.
|
|
79
|
+
serverReady: z.boolean(),
|
|
80
|
+
options: z.array(LocalModelOptionFitSchema),
|
|
81
|
+
// The two the connect view offers by name. Each is a model plus the window it was priced at, ready to be added
|
|
82
|
+
// verbatim; absent where nothing on the list fits this machine, which is an answer, not an error.
|
|
83
|
+
instant: z.object({ model: z.string(), context: z.string() }).optional(),
|
|
84
|
+
best: z.object({ model: z.string(), context: z.string() }).optional(),
|
|
85
|
+
prefetch: LocalModelPrefetchSchema,
|
|
86
|
+
});
|
|
87
|
+
export type LocalModelFitResponse = z.infer<typeof LocalModelFitSchema>;
|
|
88
|
+
|
|
30
89
|
export const endpointsContract = {
|
|
31
90
|
models: oc
|
|
32
91
|
.route({
|
|
@@ -47,4 +106,27 @@ export const endpointsContract = {
|
|
|
47
106
|
"The allowance, what has been used, when it resets, and which model actually answered the last message. Not being available is the ordinary answer rather than a failure: most sandboxes run against a platform that offers no trial at all.",
|
|
48
107
|
})
|
|
49
108
|
.output(TrialStatusSchema),
|
|
109
|
+
// Sized against this machine rather than a table in the browser, because the numbers that matter (a cgroup cap, a
|
|
110
|
+
// GPU that may not be passed through, weights already on disk) are only knowable in here.
|
|
111
|
+
localModelFit: oc
|
|
112
|
+
.route({
|
|
113
|
+
method: "GET",
|
|
114
|
+
path: "/endpoints/local-model/fit",
|
|
115
|
+
summary: "Which local models this machine can actually run",
|
|
116
|
+
description:
|
|
117
|
+
"The memory this sandbox may use, whether a GPU reached it, and every curated model priced against both, plus the two the connect view offers: one that downloads in a minute and one that is the best this machine can hold. Nothing here is a preference; it is what a start would be admitted or refused on.",
|
|
118
|
+
})
|
|
119
|
+
.output(LocalModelFitSchema),
|
|
120
|
+
// Start or stop fetching the instant model's weights before anyone has asked for them, so the first local turn does
|
|
121
|
+
// not begin with a download. Idempotent: starting twice joins the transfer already running.
|
|
122
|
+
localModelPrefetch: oc
|
|
123
|
+
.route({
|
|
124
|
+
method: "POST",
|
|
125
|
+
path: "/endpoints/local-model/prefetch",
|
|
126
|
+
summary: "Fetch the small model's weights ahead of being asked",
|
|
127
|
+
description:
|
|
128
|
+
"Downloads the curated instant model into the workspace cache so that adding it later costs nothing. Stopping leaves the part file, so a later start resumes from where this one stopped rather than beginning again.",
|
|
129
|
+
})
|
|
130
|
+
.input(z.object({ action: z.enum(["start", "stop"]) }))
|
|
131
|
+
.output(LocalModelPrefetchSchema),
|
|
50
132
|
};
|
|
@@ -7,6 +7,11 @@ import { ModelsSchema } from "../schemas/providers/provider-oauth.js";
|
|
|
7
7
|
// this sandbox serves. Deliberately not the capability list (/capabilities, maintainer): a name and a label say what a
|
|
8
8
|
// chat may run on, and say nothing about what holds a credential or what this box can reach.
|
|
9
9
|
export const RunnableProvidersSchema = z.object({
|
|
10
|
+
// The one thing about the fixed native list this route says, and it says no more than the ACP and endpoint lists
|
|
11
|
+
// do: a name a message can be addressed to. Which account answers for it, and whose it is, stays behind
|
|
12
|
+
// /accounts, whose tier is a different question. Without this a reader who may drive a turn but not read that
|
|
13
|
+
// route sees no native provider as runnable and is told to connect one the box already has.
|
|
14
|
+
native: z.array(NativeProviderParamSchema.shape.provider).describe("Native providers with a working credential here, by id; never what holds it."),
|
|
10
15
|
agents: z
|
|
11
16
|
.array(z.object({ id: z.string(), label: z.string() }))
|
|
12
17
|
.describe("ACP agents installed here. The id is the provider id itself, the label its display name."),
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { FIELD_NOTES_FILE } from "@intentic/constants";
|
|
1
2
|
import { oc } from "@orpc/contract";
|
|
2
3
|
import {
|
|
3
4
|
BuiltinPromptSchema,
|
|
4
5
|
BuiltinPromptTextSchema,
|
|
6
|
+
FieldNotesStatusSchema,
|
|
5
7
|
REPO_CHECKS_FILE,
|
|
6
8
|
RepoChecksAdoptSchema,
|
|
7
9
|
RepoChecksListSchema,
|
|
8
10
|
RuleFiringsSchema,
|
|
9
11
|
SandboxSettingsSchema,
|
|
10
12
|
SavingsReportSchema,
|
|
13
|
+
TimezoneOfferSchema,
|
|
14
|
+
TimezoneStateSchema,
|
|
11
15
|
} from "../schemas/settings.js";
|
|
12
16
|
import { OkSchema } from "../schemas/shared.js";
|
|
13
17
|
import { DayWindowQuerySchema } from "../schemas/providers/usage.js";
|
|
@@ -73,6 +77,30 @@ export const settingsContract = {
|
|
|
73
77
|
description: `Every repository that declares its own checks at \`${REPO_CHECKS_FILE}\`, what it declares, and whether you have switched it on. A repository declares what to run because the command belongs beside the scripts it names; nothing it declares runs until you say so.`,
|
|
74
78
|
})
|
|
75
79
|
.output(RepoChecksListSchema),
|
|
80
|
+
// Read off the file and the automation store rather than the settings manifest: nothing here is a choice anyone
|
|
81
|
+
// made, so storing it would only give it a second place to be wrong.
|
|
82
|
+
fieldNotes: oc
|
|
83
|
+
.route({
|
|
84
|
+
method: "GET",
|
|
85
|
+
path: "/settings/field-notes",
|
|
86
|
+
summary: "The state of this sandbox's field notes",
|
|
87
|
+
description: `Whether \`${FIELD_NOTES_FILE}\` exists, when it was last rewritten, how much of it the current budget reaches, and whether a monthly rewrite is scheduled.`,
|
|
88
|
+
})
|
|
89
|
+
.output(FieldNotesStatusSchema),
|
|
90
|
+
// Its own route rather than a field on `set`, for one reason: this is the only write that must NOT overwrite. The
|
|
91
|
+
// browser offers the zone it is in; the daemon takes it only if nobody has chosen one. Folding it into the
|
|
92
|
+
// whole-object `set` would mean the second machine to open the workspace silently moved every schedule to its own
|
|
93
|
+
// clock, and an owner in Warsaw would find their chores on a colleague's Tokyo hours.
|
|
94
|
+
adoptTimezone: oc
|
|
95
|
+
.route({
|
|
96
|
+
method: "POST",
|
|
97
|
+
path: "/settings/timezone",
|
|
98
|
+
summary: "Offer this sandbox a clock, if it has none",
|
|
99
|
+
description:
|
|
100
|
+
"Sets which timezone this sandbox's schedules are meant in, but only while it has none set. Already answered, the stored zone wins and comes back unchanged, so any number of browsers can offer theirs without fighting over it. To change a zone that is already set, write the settings.",
|
|
101
|
+
})
|
|
102
|
+
.input(TimezoneOfferSchema)
|
|
103
|
+
.output(TimezoneStateSchema),
|
|
76
104
|
adoptRepoChecks: oc
|
|
77
105
|
.route({
|
|
78
106
|
method: "POST",
|
|
@@ -250,7 +250,10 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
250
250
|
resetsAt: z.number().optional(),
|
|
251
251
|
// Gated codes only: "scheduled" if this conversation arms the resume, "available" if one exists but isn't.
|
|
252
252
|
autoResume: z.enum(["scheduled", "available"]).optional(),
|
|
253
|
-
//
|
|
253
|
+
// "scheduled" only: epoch seconds the booked send actually fires. Distinct from `resetsAt` (the provider's fact
|
|
254
|
+
// about the allowance) since a ladder rung can fall short of it, and a stopped turn has no allowance at all.
|
|
255
|
+
nextAt: z.number().optional(),
|
|
256
|
+
// A held turn, whatever wall stopped it: how re-running the exact turn would go, and what it would cost.
|
|
254
257
|
held: z
|
|
255
258
|
.object({
|
|
256
259
|
ran: z.boolean(),
|
package/src/events/transcript.ts
CHANGED
|
@@ -228,7 +228,7 @@ export const TranscriptRowSchema = z.object({
|
|
|
228
228
|
),
|
|
229
229
|
// The one-press follow-up this notice offers, by name; the chat decides what it does, and whether it stands.
|
|
230
230
|
noticeAction: z
|
|
231
|
-
.enum(["landHold", "
|
|
231
|
+
.enum(["landHold", "depsInstall", "watchStop"])
|
|
232
232
|
.optional()
|
|
233
233
|
.describe("A one-press follow-up this notice offers, by name. The chat decides what it does and whether it still applies."),
|
|
234
234
|
// An unfinished wait this notice describes, by name; whether it's still running is live state, not stored here.
|
|
@@ -329,6 +329,14 @@ export const TurnEndingSchema = z.object({
|
|
|
329
329
|
.boolean()
|
|
330
330
|
.optional()
|
|
331
331
|
.describe("Whether something other than the user is already booked to send this turn again, so the surface reports the wait instead of offering a press."),
|
|
332
|
+
// `resetsAt` is the provider's fact about the allowance; this is the daemon's own appointment, and the two differ
|
|
333
|
+
// whenever a ladder rung falls short of the reset, or an ending with no allowance at all is armed.
|
|
334
|
+
nextAt: z
|
|
335
|
+
.number()
|
|
336
|
+
.optional()
|
|
337
|
+
.describe(
|
|
338
|
+
"When the booked send actually fires, in epoch seconds. Present only with `scheduled`; absent for a booking that fires on the next pass, which is 'now' to a reader.",
|
|
339
|
+
),
|
|
332
340
|
});
|
|
333
341
|
export type TurnEnding = z.infer<typeof TurnEndingSchema>;
|
|
334
342
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { activityContract } from "./contracts/activity.contract.js";
|
|
|
5
5
|
import { agentContract } from "./contracts/agent.contract.js";
|
|
6
6
|
import { agentsContract } from "./contracts/agents.contract.js";
|
|
7
7
|
import { approvalsContract } from "./contracts/approvals.contract.js";
|
|
8
|
+
import { areasContract } from "./contracts/areas.contract.js";
|
|
8
9
|
import { automationsContract } from "./contracts/automations.contract.js";
|
|
9
10
|
import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
10
11
|
import { choresContract } from "./contracts/chores.contract.js";
|
|
@@ -46,11 +47,23 @@ export { activityContract } from "./contracts/activity.contract.js";
|
|
|
46
47
|
export { agentContract } from "./contracts/agent.contract.js";
|
|
47
48
|
export { agentsContract } from "./contracts/agents.contract.js";
|
|
48
49
|
export { approvalsContract } from "./contracts/approvals.contract.js";
|
|
50
|
+
export { areasContract } from "./contracts/areas.contract.js";
|
|
49
51
|
export { automationsContract } from "./contracts/automations.contract.js";
|
|
50
52
|
export { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
51
53
|
export { choresContract } from "./contracts/chores.contract.js";
|
|
52
54
|
export { ciContract } from "./contracts/ci.contract.js";
|
|
53
|
-
export {
|
|
55
|
+
export {
|
|
56
|
+
endpointsContract,
|
|
57
|
+
LocalModelFitSchema,
|
|
58
|
+
type LocalModelFitResponse,
|
|
59
|
+
type LocalModelGpu,
|
|
60
|
+
LocalModelGpuSchema,
|
|
61
|
+
type LocalModelPrefetch,
|
|
62
|
+
LocalModelPrefetchSchema,
|
|
63
|
+
type TrialHealth,
|
|
64
|
+
TrialStatusSchema,
|
|
65
|
+
type TrialStatusResponse,
|
|
66
|
+
} from "./contracts/endpoints.contract.js";
|
|
54
67
|
export { exitContract } from "./contracts/exit.contract.js";
|
|
55
68
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
56
69
|
export { personasContract } from "./contracts/personas.contract.js";
|
|
@@ -143,6 +156,9 @@ export * from "./policy/approvals-execution.js";
|
|
|
143
156
|
// flat object, split back into path/body/query. `schemas/internal.ts` is absent: it's vocabulary, not a wire shape.
|
|
144
157
|
export * from "./schemas/activity.js";
|
|
145
158
|
export * from "./schemas/agent.js";
|
|
159
|
+
export * from "./schemas/areas.js";
|
|
160
|
+
export * from "./policy/fence-paths.js";
|
|
161
|
+
export * from "./policy/persona-home.js";
|
|
146
162
|
export * from "./schemas/agents.js";
|
|
147
163
|
export * from "./schemas/approvals.js";
|
|
148
164
|
export * from "./schemas/automations.js";
|
|
@@ -186,6 +202,7 @@ export * from "./schemas/share.js";
|
|
|
186
202
|
export * from "./schemas/shared.js";
|
|
187
203
|
export * from "./schemas/system.js";
|
|
188
204
|
export * from "./schemas/terminal.js";
|
|
205
|
+
export * from "./schemas/turn-break.js";
|
|
189
206
|
export * from "./schemas/providers/usage.js";
|
|
190
207
|
export * from "./schemas/vpn.js";
|
|
191
208
|
export * from "./schemas/webext.js";
|
|
@@ -205,6 +222,7 @@ export * from "./text/model-answer.js";
|
|
|
205
222
|
export * from "./text/whisper.js";
|
|
206
223
|
export * from "./text/workflow-faults.js";
|
|
207
224
|
export * from "./text/emoji.js";
|
|
225
|
+
export * from "./time/zone.js";
|
|
208
226
|
|
|
209
227
|
// The aggregated contract; implemented server-side by per-domain route factories, consumed by the browser's typed oRPC
|
|
210
228
|
// client. Mounted at the sandbox root, alongside plain Hono routes like /health and /workspace/raw.
|
|
@@ -214,6 +232,7 @@ export const sandboxContract = {
|
|
|
214
232
|
agent: agentContract,
|
|
215
233
|
agents: agentsContract,
|
|
216
234
|
approvals: approvalsContract,
|
|
235
|
+
areas: areasContract,
|
|
217
236
|
automations: automationsContract,
|
|
218
237
|
capabilities: capabilitiesContract,
|
|
219
238
|
chores: choresContract,
|
|
@@ -156,7 +156,7 @@ export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
|
|
|
156
156
|
// Every reasoning tier a catalog row's `efforts` may name, weakest first. One vocabulary for every provider, so the
|
|
157
157
|
// picker can label and order a ladder it has never seen; a provider spelling a rung its own way translates on the way
|
|
158
158
|
// in, at the adapter that read it.
|
|
159
|
-
export const EFFORT_TIERS: readonly string[] = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
159
|
+
export const EFFORT_TIERS: readonly string[] = ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
160
160
|
|
|
161
161
|
// Filters exactly one pair: Claude's `max` with thinking explicitly false, the one combination Anthropic's API refuses
|
|
162
162
|
// (400). Absent thinking is not off; the model's own default answers, every other tier or provider passes through.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fenceAllows, fenceCovers, fenceIntersection, fenceReaches, foldPath, pathInsideFolder } from "./fence-paths.js";
|
|
3
|
+
|
|
4
|
+
describe("foldPath", () => {
|
|
5
|
+
test("folds the spellings of one path into one answer", () => {
|
|
6
|
+
expect(foldPath("a/./b")).toBe("a/b");
|
|
7
|
+
expect(foldPath("a//b/")).toBe("a/b");
|
|
8
|
+
expect(foldPath("./a/b")).toBe("a/b");
|
|
9
|
+
expect(foldPath("a\\b")).toBe("a/b");
|
|
10
|
+
expect(foldPath("a/c/../b")).toBe("a/b");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("the workspace root folds to the empty string, which is an answer and not a miss", () => {
|
|
14
|
+
expect(foldPath(".")).toBe("");
|
|
15
|
+
expect(foldPath("")).toBe("");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// A path that climbs out, or opens at a root, names something this workspace does not own; the fence has no say
|
|
19
|
+
// over it and must not be tricked into answering as if it did.
|
|
20
|
+
test("a path that leaves the workspace has no folded form", () => {
|
|
21
|
+
expect(foldPath("../elsewhere")).toBeUndefined();
|
|
22
|
+
expect(foldPath("a/../../elsewhere")).toBeUndefined();
|
|
23
|
+
expect(foldPath("/etc/passwd")).toBeUndefined();
|
|
24
|
+
expect(foldPath("C:/Windows")).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("pathInsideFolder", () => {
|
|
29
|
+
test("a folder holds itself and everything under it", () => {
|
|
30
|
+
expect(pathInsideFolder("apps/web", "apps/web")).toBe(true);
|
|
31
|
+
expect(pathInsideFolder("apps/web", "apps/web/src/main.ts")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// The whole reason this is segment arithmetic rather than a string prefix.
|
|
35
|
+
test("a sibling sharing a prefix is not inside", () => {
|
|
36
|
+
expect(pathInsideFolder("apps/web", "apps/web2/main.ts")).toBe(false);
|
|
37
|
+
expect(pathInsideFolder("app", "app2")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("the workspace root holds every path in the workspace", () => {
|
|
41
|
+
expect(pathInsideFolder("", "anything/at/all")).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a climbing path is inside nothing", () => {
|
|
45
|
+
expect(pathInsideFolder("apps/web", "apps/web/../../secrets")).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("fenceAllows", () => {
|
|
50
|
+
test("no fence is the whole workspace", () => {
|
|
51
|
+
expect(fenceAllows(undefined, "finance/ledger.csv")).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// The distinction the type exists for: undefined is "unfenced", [] is "fenced to nothing".
|
|
55
|
+
test("an empty fence admits nothing", () => {
|
|
56
|
+
expect(fenceAllows([], "anything")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a fence admits its own folders and refuses the rest", () => {
|
|
60
|
+
expect(fenceAllows(["support", "docs"], "support/tickets/1.md")).toBe(true);
|
|
61
|
+
expect(fenceAllows(["support", "docs"], "finance/payroll.csv")).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("fenceReaches", () => {
|
|
66
|
+
// Without this a fence on `finance/reports` would hide `finance` and leave the folder unreachable in a tree.
|
|
67
|
+
test("an ancestor of an allowed folder may still be listed", () => {
|
|
68
|
+
expect(fenceReaches(["finance/reports"], "finance")).toBe(true);
|
|
69
|
+
expect(fenceReaches(["finance/reports"], "")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a sibling of an allowed folder is not reachable", () => {
|
|
73
|
+
expect(fenceReaches(["finance/reports"], "finance/payroll")).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("fenceCovers", () => {
|
|
78
|
+
test("the unfenced cover everything, and are covered by nothing else", () => {
|
|
79
|
+
expect(fenceCovers(undefined, ["anything"])).toBe(true);
|
|
80
|
+
expect(fenceCovers(["anything"], undefined)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a fence covers another when every folder of it is inside one of its own", () => {
|
|
84
|
+
expect(fenceCovers(["apps"], ["apps/web", "apps/api"])).toBe(true);
|
|
85
|
+
expect(fenceCovers(["apps/web"], ["apps"])).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a fence covers the empty one, since nothing is inside everything", () => {
|
|
89
|
+
expect(fenceCovers(["apps/web"], [])).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("fenceIntersection", () => {
|
|
94
|
+
test("either side alone narrows to itself", () => {
|
|
95
|
+
expect(fenceIntersection(undefined, ["apps/web"])).toEqual(["apps/web"]);
|
|
96
|
+
expect(fenceIntersection(["apps/web"], undefined)).toEqual(["apps/web"]);
|
|
97
|
+
expect(fenceIntersection(undefined, undefined)).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// The narrower of the pair survives, whichever side it came from, so a persona asking for more than its starter
|
|
101
|
+
// holds gets the starter's answer.
|
|
102
|
+
test("the tighter folder is what survives, from whichever side", () => {
|
|
103
|
+
expect(fenceIntersection(["apps"], ["apps/web"])).toEqual(["apps/web"]);
|
|
104
|
+
expect(fenceIntersection(["apps/web"], ["apps"])).toEqual(["apps/web"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("disjoint fences meet at nothing, which is a fence admitting nothing", () => {
|
|
108
|
+
expect(fenceIntersection(["apps/web"], ["finance"])).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("overlapping folders are not repeated", () => {
|
|
112
|
+
expect(fenceIntersection(["apps", "apps/web"], ["apps/web"])).toEqual(["apps/web"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { PUBLIC_DIR, STATE_DIR } from "@intentic/constants";
|
|
2
|
+
|
|
3
|
+
// The path arithmetic a workspace fence is made of: whether a path is inside a folder, whether one set of folders
|
|
4
|
+
// covers another, and what a fenced tree may still list. Pure and shared, because the daemon refuses on these answers
|
|
5
|
+
// and the browser draws on them, and two implementations would disagree at exactly the boundary that matters.
|
|
6
|
+
|
|
7
|
+
// Workspace-relative, forward-slash, no climb: segments are folded so `a/./b` and `a/b` are one path, and a path that
|
|
8
|
+
// climbs above the root folds to `undefined` rather than to something inside it.
|
|
9
|
+
// The root itself is "" — a real answer, not a missing one.
|
|
10
|
+
export const foldPath = (raw: string): string | undefined => {
|
|
11
|
+
if (raw.startsWith("/") || /^[A-Za-z]:/.test(raw)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const segments: string[] = [];
|
|
15
|
+
for (const segment of raw.split(/[\\/]/)) {
|
|
16
|
+
if (segment === "" || segment === ".") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (segment !== "..") {
|
|
20
|
+
segments.push(segment);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (segments.pop() === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return segments.join("/");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Whether `path` is `folder` or lives under it, by segment rather than by string prefix: `app2` is not inside `app`.
|
|
31
|
+
// An unfoldable path is inside nothing.
|
|
32
|
+
export const pathInsideFolder = (folder: string, path: string): boolean => {
|
|
33
|
+
const outer = foldPath(folder);
|
|
34
|
+
const inner = foldPath(path);
|
|
35
|
+
if (outer === undefined || inner === undefined) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return outer === "" || inner === outer || inner.startsWith(`${outer}/`);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// "Change the sandbox" as paths: the daemon's own configuration, and the outbox the internet reads. One list, read by
|
|
42
|
+
// the persona hook that judges a turn's writes and by the area schema that decides what a grant may name.
|
|
43
|
+
export const SANDBOX_PATHS: readonly string[] = [STATE_DIR, PUBLIC_DIR];
|
|
44
|
+
|
|
45
|
+
// Whether a path is one of those folders or sits inside one, by segment: `publications` is not `public`.
|
|
46
|
+
// No area may name such a folder, which is what keeps a fence from handing anyone the config that decides what agents
|
|
47
|
+
// may do — the enumerated control-plane table (state/workspace-state.ts) locks named entries, not the whole tree.
|
|
48
|
+
export const isSandboxPath = (path: string): boolean => SANDBOX_PATHS.some((folder) => pathInsideFolder(folder, path));
|
|
49
|
+
|
|
50
|
+
// A fence: the folders a caller may touch, or undefined for the whole workspace. Undefined and `[]` are deliberately
|
|
51
|
+
// different answers — no fence at all, versus a fence that admits nothing.
|
|
52
|
+
export type Fence = readonly string[] | undefined;
|
|
53
|
+
|
|
54
|
+
/** Whether the fence admits this path at all: reading it, writing it, searching in it. */
|
|
55
|
+
export const fenceAllows = (fence: Fence, path: string): boolean => fence === undefined || fence.some((folder) => pathInsideFolder(folder, path));
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether a fenced tree may still LIST this path: everything the fence allows, plus the ancestors leading down to it.
|
|
59
|
+
* Without the ancestors a fence on `finance/reports` would hide `finance` itself and leave the folder unreachable.
|
|
60
|
+
*/
|
|
61
|
+
export const fenceReaches = (fence: Fence, path: string): boolean => {
|
|
62
|
+
if (fenceAllows(fence, path)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const inner = foldPath(path);
|
|
66
|
+
return inner !== undefined && (fence ?? []).some((folder) => pathInsideFolder(inner, folder));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether `outer` covers `inner`: every folder the inner fence admits is one the outer fence admits too. The question
|
|
71
|
+
* behind every narrowing-only rule — handing work across, inheriting a fence — since a fence that covers another can
|
|
72
|
+
* safely stand in for it.
|
|
73
|
+
*/
|
|
74
|
+
export const fenceCovers = (outer: Fence, inner: Fence): boolean => {
|
|
75
|
+
if (outer === undefined) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (inner === undefined) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return inner.every((folder) => fenceAllows(outer, folder));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The tighter of two fences: what a turn may touch when a persona's folders meet the member's own. Undefined on both
|
|
86
|
+
* sides is the whole workspace; either side alone narrows to itself.
|
|
87
|
+
*/
|
|
88
|
+
export const fenceIntersection = (left: Fence, right: Fence): Fence => {
|
|
89
|
+
if (left === undefined) {
|
|
90
|
+
return right;
|
|
91
|
+
}
|
|
92
|
+
if (right === undefined) {
|
|
93
|
+
return left;
|
|
94
|
+
}
|
|
95
|
+
// A folder survives only where one side contains it; the containing side's folder is already the narrower of the
|
|
96
|
+
// pair, so taking each side's contained folders and deduping gives the intersection with no overlap left over.
|
|
97
|
+
const kept = [...left.filter((folder) => fenceAllows(right, folder)), ...right.filter((folder) => fenceAllows(left, folder))];
|
|
98
|
+
return [...new Set(kept.map((folder) => foldPath(folder) ?? folder))];
|
|
99
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fenceHoldsPersona, personaHome } from "./persona-home.js";
|
|
3
|
+
|
|
4
|
+
// Where a card lives, and therefore who may wear it. This is the whole of the rule the Access tab draws and the
|
|
5
|
+
// daemon refuses on, so the three shapes a card can name a place in are each pinned here.
|
|
6
|
+
|
|
7
|
+
describe("personaHome", () => {
|
|
8
|
+
test("the folder it opens in is its home, even when it may touch more", () => {
|
|
9
|
+
expect(personaHome({ workspace: { startIn: "support" } })).toEqual(["support"]);
|
|
10
|
+
expect(personaHome({ workspace: { startIn: "support", folders: ["support", "shared"] } })).toEqual(["support"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("with no starting folder, the folders it may touch are its home", () => {
|
|
14
|
+
expect(personaHome({ workspace: { folders: ["finance", "billing"] } })).toEqual(["finance", "billing"]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("naming no place at all is the workspace root, which is not a folder any fence can hold", () => {
|
|
18
|
+
expect(personaHome({})).toBeUndefined();
|
|
19
|
+
expect(personaHome({ workspace: {} })).toBeUndefined();
|
|
20
|
+
expect(personaHome({ workspace: { folders: [] } })).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("fenceHoldsPersona", () => {
|
|
25
|
+
const support = { workspace: { startIn: "support/inbox" } };
|
|
26
|
+
const finance = { workspace: { startIn: "finance" } };
|
|
27
|
+
const anywhere = { workspace: { folders: ["finance", "support"] } };
|
|
28
|
+
|
|
29
|
+
test("an unfenced holder wears every card, root-homed ones included", () => {
|
|
30
|
+
expect(fenceHoldsPersona(undefined, support)).toBe(true);
|
|
31
|
+
expect(fenceHoldsPersona(undefined, {})).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a fenced holder wears the cards homed inside its folders and no others", () => {
|
|
35
|
+
expect(fenceHoldsPersona(["support"], support)).toBe(true);
|
|
36
|
+
expect(fenceHoldsPersona(["support"], finance)).toBe(false);
|
|
37
|
+
// By segment, not by string prefix: `support2` is not inside `support`.
|
|
38
|
+
expect(fenceHoldsPersona(["support"], { workspace: { startIn: "support2" } })).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a card spanning more than the fence is not held: the fence has to cover all of it", () => {
|
|
42
|
+
expect(fenceHoldsPersona(["support"], anywhere)).toBe(false);
|
|
43
|
+
expect(fenceHoldsPersona(["support", "finance"], anywhere)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// The one card no fence can reach, whatever it names: everything is inside the root, so holding it would be
|
|
47
|
+
// holding the workspace.
|
|
48
|
+
test("a card homed at the root is the owner's alone", () => {
|
|
49
|
+
expect(fenceHoldsPersona(["support"], {})).toBe(false);
|
|
50
|
+
expect(fenceHoldsPersona([], {})).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|