arcp 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +31 -0
- data/CONFORMANCE.md +71 -0
- data/LICENSE +202 -0
- data/README.md +135 -0
- data/lib/arcp/auth/auth_scheme.rb +16 -0
- data/lib/arcp/auth/bearer.rb +38 -0
- data/lib/arcp/auth.rb +4 -0
- data/lib/arcp/client.rb +354 -0
- data/lib/arcp/clock.rb +35 -0
- data/lib/arcp/credential.rb +65 -0
- data/lib/arcp/credential_provisioner.rb +132 -0
- data/lib/arcp/envelope.rb +115 -0
- data/lib/arcp/errors.rb +154 -0
- data/lib/arcp/ids.rb +18 -0
- data/lib/arcp/job/accepted.rb +37 -0
- data/lib/arcp/job/agent_ref.rb +18 -0
- data/lib/arcp/job/cancel.rb +18 -0
- data/lib/arcp/job/event.rb +68 -0
- data/lib/arcp/job/event_body/delegate.rb +24 -0
- data/lib/arcp/job/event_body/log.rb +20 -0
- data/lib/arcp/job/event_body/metric.rb +20 -0
- data/lib/arcp/job/event_body/progress.rb +27 -0
- data/lib/arcp/job/event_body/result_chunk.rb +42 -0
- data/lib/arcp/job/event_body/status.rb +25 -0
- data/lib/arcp/job/event_body/thought.rb +12 -0
- data/lib/arcp/job/event_body/tool_call.rb +16 -0
- data/lib/arcp/job/event_body/tool_result.rb +23 -0
- data/lib/arcp/job/event_body/trace_span.rb +30 -0
- data/lib/arcp/job/handle.rb +16 -0
- data/lib/arcp/job/job_error.rb +31 -0
- data/lib/arcp/job/result.rb +30 -0
- data/lib/arcp/job/submit.rb +32 -0
- data/lib/arcp/job/subscribe.rb +22 -0
- data/lib/arcp/job/subscribed.rb +14 -0
- data/lib/arcp/job/summary.rb +27 -0
- data/lib/arcp/job/unsubscribe.rb +10 -0
- data/lib/arcp/job.rb +15 -0
- data/lib/arcp/lease.rb +212 -0
- data/lib/arcp/message_types.rb +35 -0
- data/lib/arcp/runtime/credential_registry.rb +67 -0
- data/lib/arcp/runtime/event_log.rb +62 -0
- data/lib/arcp/runtime/job_context.rb +167 -0
- data/lib/arcp/runtime/job_manager.rb +256 -0
- data/lib/arcp/runtime/lease_manager.rb +88 -0
- data/lib/arcp/runtime/runtime.rb +125 -0
- data/lib/arcp/runtime/session_actor.rb +300 -0
- data/lib/arcp/runtime/subscription_manager.rb +57 -0
- data/lib/arcp/runtime.rb +10 -0
- data/lib/arcp/serializer.rb +43 -0
- data/lib/arcp/session/ack.rb +14 -0
- data/lib/arcp/session/agent_inventory.rb +51 -0
- data/lib/arcp/session/bye.rb +10 -0
- data/lib/arcp/session/capability_set.rb +29 -0
- data/lib/arcp/session/feature.rb +25 -0
- data/lib/arcp/session/hello.rb +34 -0
- data/lib/arcp/session/jobs_response.rb +18 -0
- data/lib/arcp/session/list_jobs.rb +19 -0
- data/lib/arcp/session/ping.rb +14 -0
- data/lib/arcp/session/pong.rb +14 -0
- data/lib/arcp/session/session_error.rb +23 -0
- data/lib/arcp/session/welcome.rb +38 -0
- data/lib/arcp/session.rb +26 -0
- data/lib/arcp/trace.rb +51 -0
- data/lib/arcp/transport/base.rb +29 -0
- data/lib/arcp/transport/memory_transport.rb +54 -0
- data/lib/arcp/transport/stdio_transport.rb +56 -0
- data/lib/arcp/transport/websocket_transport.rb +47 -0
- data/lib/arcp/transport.rb +6 -0
- data/lib/arcp/version.rb +7 -0
- data/lib/arcp.rb +19 -0
- data/sig/arcp/client.rbs +16 -0
- data/sig/arcp/credential.rbs +51 -0
- data/sig/arcp/envelope.rbs +25 -0
- data/sig/arcp/errors.rbs +40 -0
- data/sig/arcp/job.rbs +83 -0
- data/sig/arcp/lease.rbs +41 -0
- data/sig/arcp/runtime.rbs +41 -0
- data/sig/arcp/serializer.rbs +8 -0
- data/sig/arcp/session.rbs +56 -0
- data/sig/arcp/transport.rbs +18 -0
- data/sig/arcp.rbs +5 -0
- metadata +226 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'async'
|
|
4
|
+
require 'async/queue'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Arcp
|
|
8
|
+
module Runtime
|
|
9
|
+
AgentRegistration = Data.define(:name, :versions, :default, :handler)
|
|
10
|
+
|
|
11
|
+
JobRecord = Data.define(:job_id, :agent, :principal_id, :status, :created_at,
|
|
12
|
+
:input, :submitter_session_id, :task) do
|
|
13
|
+
def with(**kw) = self.class.new(**to_h, **kw)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Owns agent registry + per-job lifecycle. Submitted jobs run as
|
|
17
|
+
# child `Async::Task`s; cancellation propagates via `task.stop`.
|
|
18
|
+
class JobManager
|
|
19
|
+
attr_reader :runtime
|
|
20
|
+
|
|
21
|
+
def initialize(runtime:, lease_manager:, subscription_manager:, event_log:, clock: Arcp::SystemClock.new)
|
|
22
|
+
@runtime = runtime
|
|
23
|
+
@leases = lease_manager
|
|
24
|
+
@subs = subscription_manager
|
|
25
|
+
@event_log = event_log
|
|
26
|
+
@clock = clock
|
|
27
|
+
@agents = {} # name => AgentRegistration
|
|
28
|
+
@jobs = {} # job_id => JobRecord
|
|
29
|
+
@event_seq = Hash.new(0) # job_id => last emitted seq
|
|
30
|
+
@idempotency = {} # [principal, key] => job_id
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register_agent(name:, versions:, default:, handler:)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@agents[name] = AgentRegistration.new(
|
|
37
|
+
name: name, versions: Array(versions).freeze,
|
|
38
|
+
default: default, handler: handler
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def agent_inventory
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
Arcp::Session::AgentInventory.new(
|
|
46
|
+
entries: @agents.values.map do |reg|
|
|
47
|
+
Arcp::Session::AgentEntry.new(name: reg.name, versions: reg.versions, default: reg.default)
|
|
48
|
+
end.freeze
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resolve_agent(ref_str)
|
|
54
|
+
ref = Arcp::Job::AgentRef.parse(ref_str)
|
|
55
|
+
reg = @mutex.synchronize { @agents[ref.name] }
|
|
56
|
+
raise Arcp::Errors::AgentNotAvailable, "agent not registered: #{ref.name}" if reg.nil?
|
|
57
|
+
|
|
58
|
+
version = ref.version || reg.default
|
|
59
|
+
if version.nil? || (!reg.versions.empty? && !reg.versions.include?(version))
|
|
60
|
+
raise Arcp::Errors::AgentVersionNotAvailable.new(
|
|
61
|
+
"agent #{ref.name} has no version #{version.inspect}",
|
|
62
|
+
details: { 'agent' => ref.name, 'version' => version, 'available' => reg.versions }
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
[reg, "#{ref.name}@#{version}"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def submit(submit:, principal_id:, session_id:, session_actor:)
|
|
70
|
+
reg, resolved = resolve_agent(submit.agent)
|
|
71
|
+
|
|
72
|
+
if submit.idempotency_key
|
|
73
|
+
key = [principal_id, submit.idempotency_key]
|
|
74
|
+
if (existing = @mutex.synchronize { @idempotency[key] })
|
|
75
|
+
existing_record = @mutex.synchronize { @jobs[existing] }
|
|
76
|
+
if existing_record && existing_record.agent != resolved
|
|
77
|
+
raise Arcp::Errors::DuplicateKey.new(
|
|
78
|
+
'idempotency key reused with different agent',
|
|
79
|
+
details: { 'job_id' => existing }
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
return existing
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
job_id = Arcp::Ids.job_id
|
|
87
|
+
|
|
88
|
+
lease = build_lease(submit, job_id)
|
|
89
|
+
@leases.register(job_id, lease) if lease
|
|
90
|
+
credentials = issue_credentials(
|
|
91
|
+
job_id: job_id, lease: lease, agent: resolved, principal_id: principal_id
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
record = JobRecord.new(
|
|
95
|
+
job_id: job_id, agent: resolved, principal_id: principal_id,
|
|
96
|
+
status: 'pending', created_at: @clock.now.iso8601,
|
|
97
|
+
input: submit.input, submitter_session_id: session_id, task: nil
|
|
98
|
+
)
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
@jobs[job_id] = record
|
|
101
|
+
@idempotency[[principal_id, submit.idempotency_key]] = job_id if submit.idempotency_key
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@subs.register_owner(job_id, principal_id, session_id, session_actor.outbox)
|
|
105
|
+
|
|
106
|
+
task = Async do |t|
|
|
107
|
+
run_agent(t, reg, job_id, submit, lease)
|
|
108
|
+
end
|
|
109
|
+
@mutex.synchronize { @jobs[job_id] = @jobs[job_id].with(task: task, status: 'running') }
|
|
110
|
+
|
|
111
|
+
[job_id, resolved, lease, credentials]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cancel(job_id:, principal_id:, reason: nil)
|
|
115
|
+
record = @mutex.synchronize { @jobs[job_id] }
|
|
116
|
+
raise Arcp::Errors::JobNotFound, "no such job: #{job_id}" unless record
|
|
117
|
+
|
|
118
|
+
unless record.principal_id == principal_id
|
|
119
|
+
raise Arcp::Errors::PermissionDenied.new(
|
|
120
|
+
'only the submitting principal can cancel a job',
|
|
121
|
+
details: { 'job_id' => job_id }
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
record.task&.stop
|
|
126
|
+
publish_error(job_id, Arcp::Job::JobError.new(
|
|
127
|
+
job_id: job_id, final_status: 'cancelled',
|
|
128
|
+
code: 'CANCELLED', message: reason, retryable: false, details: {}
|
|
129
|
+
))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def list(principal_id:, filter: {}, limit: 50, cursor: nil)
|
|
133
|
+
offset = cursor ? cursor.to_i : 0
|
|
134
|
+
rows = @mutex.synchronize do
|
|
135
|
+
@jobs.values
|
|
136
|
+
.select { |r| r.principal_id == principal_id }
|
|
137
|
+
.select { |r| filter['status'].nil? || filter['status'].include?(r.status) }
|
|
138
|
+
.select { |r| filter['agent'].nil? || r.agent.start_with?(filter['agent']) }
|
|
139
|
+
.sort_by(&:created_at)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
page = rows[offset, limit] || []
|
|
143
|
+
next_cursor = (offset + page.size) < rows.size ? (offset + page.size).to_s : nil
|
|
144
|
+
|
|
145
|
+
summaries = page.map do |r|
|
|
146
|
+
lease = @leases.get(r.job_id)
|
|
147
|
+
counter = @leases.counter(r.job_id)
|
|
148
|
+
Arcp::Job::Summary.new(
|
|
149
|
+
job_id: r.job_id, agent: r.agent, status: r.status, created_at: r.created_at,
|
|
150
|
+
lease_expires_at: lease&.expires_at,
|
|
151
|
+
budget_remaining: counter ? counter.snapshot.transform_values { |v| v.to_s('F') } : nil
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Arcp::Session::JobsResponse.new(
|
|
156
|
+
jobs: summaries.map(&:to_h), next_cursor: next_cursor
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def lookup(job_id) = @mutex.synchronize { @jobs[job_id] }
|
|
161
|
+
|
|
162
|
+
def publish_event(job_id, event)
|
|
163
|
+
seq = @mutex.synchronize { @event_seq[job_id] += 1 }
|
|
164
|
+
env = Arcp::Envelope.build(
|
|
165
|
+
type: Arcp::MessageTypes::JOB_EVENT,
|
|
166
|
+
session_id: @mutex.synchronize { @jobs[job_id]&.submitter_session_id || '' },
|
|
167
|
+
job_id: job_id, event_seq: seq, payload: event.to_h
|
|
168
|
+
)
|
|
169
|
+
@event_log.append(env.session_id, env)
|
|
170
|
+
@subs.fanout(job_id, env)
|
|
171
|
+
seq
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def publish_result(job_id, result)
|
|
175
|
+
record = @mutex.synchronize do
|
|
176
|
+
@jobs[job_id] = @jobs[job_id].with(status: 'succeeded') if @jobs[job_id]
|
|
177
|
+
@jobs[job_id]
|
|
178
|
+
end
|
|
179
|
+
env = Arcp::Envelope.build(
|
|
180
|
+
type: Arcp::MessageTypes::JOB_RESULT,
|
|
181
|
+
session_id: record&.submitter_session_id || '',
|
|
182
|
+
job_id: job_id, payload: result.to_h
|
|
183
|
+
)
|
|
184
|
+
@event_log.append(env.session_id, env)
|
|
185
|
+
@subs.fanout(job_id, env)
|
|
186
|
+
@subs.clear(job_id)
|
|
187
|
+
@runtime.credential_registry&.revoke_all(job_id: job_id)
|
|
188
|
+
@leases.revoke(job_id)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def publish_error(job_id, error)
|
|
192
|
+
record = @mutex.synchronize do
|
|
193
|
+
@jobs[job_id] = @jobs[job_id].with(status: error.final_status) if @jobs[job_id]
|
|
194
|
+
@jobs[job_id]
|
|
195
|
+
end
|
|
196
|
+
env = Arcp::Envelope.build(
|
|
197
|
+
type: Arcp::MessageTypes::JOB_ERROR,
|
|
198
|
+
session_id: record&.submitter_session_id || '',
|
|
199
|
+
job_id: job_id, payload: error.to_h
|
|
200
|
+
)
|
|
201
|
+
@event_log.append(env.session_id, env)
|
|
202
|
+
@subs.fanout(job_id, env)
|
|
203
|
+
@subs.clear(job_id)
|
|
204
|
+
@runtime.credential_registry&.revoke_all(job_id: job_id)
|
|
205
|
+
@leases.revoke(job_id)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def issue_credentials(job_id:, lease:, agent:, principal_id:)
|
|
211
|
+
return nil unless @runtime.credential_registry
|
|
212
|
+
|
|
213
|
+
@runtime.credential_registry.issue_for(
|
|
214
|
+
job_id: job_id, lease: lease, agent: agent, principal_id: principal_id
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_lease(submit, job_id)
|
|
219
|
+
return nil unless submit.lease_request
|
|
220
|
+
|
|
221
|
+
Arcp::Lease::Lease.new(
|
|
222
|
+
id: "lse_#{job_id}",
|
|
223
|
+
capabilities: submit.lease_request.capabilities,
|
|
224
|
+
budget: submit.lease_request.budget,
|
|
225
|
+
model_use: submit.lease_request.model_use,
|
|
226
|
+
expires_at: submit.lease_constraints&.expires_at || submit.lease_request.expires_at,
|
|
227
|
+
issued_at: @clock.now.iso8601
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def run_agent(task, reg, job_id, submit, lease)
|
|
232
|
+
ctx = JobContext.new(
|
|
233
|
+
job_id: job_id, agent: reg.name, input: submit.input,
|
|
234
|
+
lease: lease, sink: self
|
|
235
|
+
)
|
|
236
|
+
if submit.max_runtime_sec
|
|
237
|
+
task.async do
|
|
238
|
+
task.sleep(submit.max_runtime_sec)
|
|
239
|
+
ctx.fail!(code: 'TIMEOUT', message: 'max_runtime_sec elapsed', retryable: true)
|
|
240
|
+
task.stop
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
reg.handler.call(ctx)
|
|
245
|
+
ctx.finish unless ctx.instance_variable_get(:@done)
|
|
246
|
+
rescue Arcp::Error => e
|
|
247
|
+
ctx&.fail!(code: e.code, message: e.message, retryable: e.retryable?, details: e.details || {})
|
|
248
|
+
rescue Async::Stop
|
|
249
|
+
nil
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
ctx&.fail!(code: 'INTERNAL_ERROR', message: e.message, retryable: true,
|
|
252
|
+
details: { 'class' => e.class.name })
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Runtime
|
|
5
|
+
# Tracks per-job leases and bound budget counters. The runtime asks
|
|
6
|
+
# `#check!(job_id, capability:)` before every authority op.
|
|
7
|
+
class LeaseManager
|
|
8
|
+
def initialize(clock: Arcp::SystemClock.new, enforce_model_use: false)
|
|
9
|
+
@clock = clock
|
|
10
|
+
@enforce_model_use = enforce_model_use
|
|
11
|
+
@leases = {}
|
|
12
|
+
@counters = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(job_id, lease)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@leases[job_id] = lease
|
|
19
|
+
@counters[job_id] = Arcp::Lease::BudgetCounter.new(initial: lease.budget&.per_currency&.dup || {})
|
|
20
|
+
end
|
|
21
|
+
lease
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get(job_id) = @mutex.synchronize { @leases[job_id] }
|
|
25
|
+
def counter(job_id) = @mutex.synchronize { @counters[job_id] }
|
|
26
|
+
|
|
27
|
+
def check!(job_id, capability:)
|
|
28
|
+
lease = get(job_id)
|
|
29
|
+
return if lease.nil?
|
|
30
|
+
|
|
31
|
+
if lease.expired?(@clock.now)
|
|
32
|
+
raise Arcp::Errors::LeaseExpired.new(
|
|
33
|
+
"lease #{lease.id} expired at #{lease.expires_at}",
|
|
34
|
+
details: { 'lease_id' => lease.id }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return if lease.capabilities.include?(capability)
|
|
39
|
+
|
|
40
|
+
raise Arcp::Errors::PermissionDenied.new(
|
|
41
|
+
"capability #{capability.inspect} not in lease #{lease.id}",
|
|
42
|
+
details: { 'capability' => capability, 'lease_id' => lease.id }
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_model!(job_id, model_id:)
|
|
47
|
+
lease = get(job_id)
|
|
48
|
+
return true if lease.nil? && !@enforce_model_use
|
|
49
|
+
|
|
50
|
+
return true if lease&.model_use && Arcp::ModelPattern.match?(lease.model_use, model_id)
|
|
51
|
+
|
|
52
|
+
raise Arcp::Errors::PermissionDenied.new(
|
|
53
|
+
"model #{model_id.inspect} not permitted by lease",
|
|
54
|
+
details: { 'model' => model_id, 'lease_id' => lease&.id }
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Try to decrement the bound budget. Returns true on success, raises
|
|
59
|
+
# BudgetExhausted if no balance covers the amount. Straight-line —
|
|
60
|
+
# no scheduler-yielding calls between read and write.
|
|
61
|
+
def try_spend!(job_id, currency, amount)
|
|
62
|
+
counter = self.counter(job_id)
|
|
63
|
+
return true if counter.nil?
|
|
64
|
+
return true if counter.get(currency).zero? && !counter.remaining.key?(currency)
|
|
65
|
+
|
|
66
|
+
unless counter.try_decrement(currency, amount)
|
|
67
|
+
raise Arcp::Errors::BudgetExhausted.new(
|
|
68
|
+
"budget #{currency} exhausted",
|
|
69
|
+
details: { 'currency' => currency, 'remaining' => counter.get(currency).to_s('F') }
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def remaining(job_id)
|
|
76
|
+
c = counter(job_id)
|
|
77
|
+
c ? c.snapshot : {}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def revoke(job_id)
|
|
81
|
+
@mutex.synchronize do
|
|
82
|
+
@leases.delete(job_id)
|
|
83
|
+
@counters.delete(job_id)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'async'
|
|
4
|
+
|
|
5
|
+
require_relative '../envelope'
|
|
6
|
+
require_relative '../session'
|
|
7
|
+
require_relative '../job'
|
|
8
|
+
require_relative '../lease'
|
|
9
|
+
require_relative '../credential_provisioner'
|
|
10
|
+
require_relative '../auth'
|
|
11
|
+
require_relative '../clock'
|
|
12
|
+
require_relative '../message_types'
|
|
13
|
+
require_relative 'credential_registry'
|
|
14
|
+
|
|
15
|
+
module Arcp
|
|
16
|
+
module Runtime
|
|
17
|
+
# ARCP runtime. Owns the agent registry, job manager, lease manager,
|
|
18
|
+
# subscription manager, and event log. Sessions attach via
|
|
19
|
+
# `#accept(transport)` which returns an `Async::Task` running the
|
|
20
|
+
# `SessionActor` for that connection.
|
|
21
|
+
class Runtime
|
|
22
|
+
attr_reader :auth_verifier, :clock, :name, :version,
|
|
23
|
+
:heartbeat_interval_sec, :resume_window_sec,
|
|
24
|
+
:job_manager, :lease_manager, :subscription_manager,
|
|
25
|
+
:event_log, :credential_registry, :enforce_model_use
|
|
26
|
+
|
|
27
|
+
def initialize(auth_verifier:, name: 'arcp-runtime', version: Arcp::VERSION,
|
|
28
|
+
heartbeat_interval_sec: 30, resume_window_sec: 300,
|
|
29
|
+
clock: Arcp::SystemClock.new, credential_provisioner: nil,
|
|
30
|
+
credential_store: nil, require_durable_store: false,
|
|
31
|
+
enforce_model_use: false)
|
|
32
|
+
if require_durable_store && credential_provisioner && credential_store.nil?
|
|
33
|
+
raise Arcp::Errors::InvalidRequest,
|
|
34
|
+
'provisioned_credentials requires a CredentialStore'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@auth_verifier = auth_verifier
|
|
38
|
+
@name = name
|
|
39
|
+
@version = version
|
|
40
|
+
@heartbeat_interval_sec = heartbeat_interval_sec
|
|
41
|
+
@resume_window_sec = resume_window_sec
|
|
42
|
+
@clock = clock
|
|
43
|
+
@enforce_model_use = enforce_model_use
|
|
44
|
+
@credential_registry = build_credential_registry(
|
|
45
|
+
credential_provisioner: credential_provisioner,
|
|
46
|
+
credential_store: credential_store,
|
|
47
|
+
clock: clock
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@event_log = EventLog.new(window_sec: resume_window_sec, clock: clock)
|
|
51
|
+
@lease_manager = LeaseManager.new(clock: clock, enforce_model_use: enforce_model_use)
|
|
52
|
+
@subscription_manager = SubscriptionManager.new
|
|
53
|
+
@job_manager = JobManager.new(
|
|
54
|
+
runtime: self,
|
|
55
|
+
lease_manager: @lease_manager,
|
|
56
|
+
subscription_manager: @subscription_manager,
|
|
57
|
+
event_log: @event_log,
|
|
58
|
+
clock: clock
|
|
59
|
+
)
|
|
60
|
+
@sessions = {}
|
|
61
|
+
@mutex = Mutex.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def register_agent(name:, versions:, default:, handler:)
|
|
65
|
+
@job_manager.register_agent(name: name, versions: versions, default: default, handler: handler)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def local_capabilities(agents_inventory: false)
|
|
69
|
+
features = Arcp::Session::Feature::ALL.dup
|
|
70
|
+
unless @credential_registry
|
|
71
|
+
features -= [
|
|
72
|
+
Arcp::Session::Feature::MODEL_USE,
|
|
73
|
+
Arcp::Session::Feature::PROVISIONED_CREDENTIALS
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Arcp::Session::CapabilitySet.local(
|
|
78
|
+
features: features,
|
|
79
|
+
agents: agents_inventory ? @job_manager.agent_inventory : nil
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def accept(transport)
|
|
84
|
+
actor = SessionActor.new(runtime: self, transport: transport)
|
|
85
|
+
actor.run
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def register_session(session_id, actor)
|
|
89
|
+
@mutex.synchronize { @sessions[session_id] = actor }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def deregister_session(session_id)
|
|
93
|
+
@mutex.synchronize { @sessions.delete(session_id) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def session(session_id) = @mutex.synchronize { @sessions[session_id] }
|
|
97
|
+
|
|
98
|
+
def shutdown(reason: nil)
|
|
99
|
+
actors = @mutex.synchronize { @sessions.values.dup }
|
|
100
|
+
actors.each { |a| a.send_envelope(bye_envelope(a.session_id, reason)) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def build_credential_registry(credential_provisioner:, credential_store:, clock:)
|
|
106
|
+
return nil unless credential_provisioner
|
|
107
|
+
|
|
108
|
+
store = credential_store || Arcp::Credentials::InMemoryStore.new
|
|
109
|
+
CredentialRegistry.new(
|
|
110
|
+
provisioner: credential_provisioner,
|
|
111
|
+
store: store,
|
|
112
|
+
clock: clock
|
|
113
|
+
).tap(&:reconcile_on_startup!)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def bye_envelope(session_id, reason)
|
|
117
|
+
Arcp::Envelope.build(
|
|
118
|
+
type: Arcp::MessageTypes::SESSION_BYE,
|
|
119
|
+
session_id: session_id,
|
|
120
|
+
payload: Arcp::Session::Bye.new(reason: reason).to_h
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|